From 19ab426a191eec575ffebe6a2417f3fce538969c Mon Sep 17 00:00:00 2001 From: bvoiturier Date: Wed, 9 Oct 2024 09:49:36 +0200 Subject: [PATCH] feat: ATL-6983 ZIO Stream Kafka PoC in background jobs (#1339) Signed-off-by: Benjamin Voiturier Signed-off-by: mineme0110 Signed-off-by: Hyperledger Bot Co-authored-by: mineme0110 Co-authored-by: Hyperledger Bot --- build.sbt | 13 +- .../src/main/resources/application.conf | 67 +++-- .../identus/agent/server/CloudAgentApp.scala | 94 +----- .../identus/agent/server/MainApp.scala | 15 +- .../agent/server/config/AppConfig.scala | 18 +- .../http/CustomServerInterceptors.scala | 93 +++--- .../server/http/ZHttp4sBlazeServer.scala | 7 +- .../server/jobs/BackgroundJobsHelper.scala | 22 +- .../server/jobs/ConnectBackgroundJobs.scala | 84 +++--- .../jobs/DIDStateSyncBackgroundJobs.scala | 51 +++- .../server/jobs/IssueBackgroundJobs.scala | 100 +++---- .../server/jobs/PresentBackgroundJobs.scala | 80 +++--- .../agent/server/jobs/StatusListJobs.scala | 268 +++++++++++------- .../controller/IssueControllerTestTools.scala | 6 +- .../OIDCCredentialIssuerServiceSpec.scala | 5 +- .../CredentialDefinitionTestTools.scala | 6 +- .../schema/CredentialSchemaTestTools.scala | 6 +- .../SystemControllerTestTools.scala | 6 +- .../VcVerificationControllerTestTools.scala | 6 +- .../sql/agent/V15__add_did_index_table.sql | 19 ++ .../service/ManagedDIDServiceImpl.scala | 19 +- ...dDIDServiceWithEventNotificationImpl.scala | 5 +- .../service/handler/DIDCreateHandler.scala | 7 +- .../sql/JdbcDIDNonSecretStorage.scala | 53 +++- .../storage/DIDNonSecretStorage.scala | 2 + .../storage/MockDIDNonSecretStorage.scala | 4 + .../core/model/WalletIdAndRecordId.scala | 19 ++ .../core/service/ConnectionServiceImpl.scala | 19 +- .../service/ConnectionServiceImplSpec.scala | 12 +- .../ConnectionServiceNotifierSpec.scala | 8 +- .../messaging/MessagingServiceTest.scala | 49 ++++ .../kafka/InMemoryMessagingServiceSpec.scala | 66 +++++ .../shared/docker-compose-with-kafka.yml | 256 +++++++++++++++++ infrastructure/shared/nginx/nginx.conf | 42 +++ .../identus/pollux/core/model/DidCommID.scala | 4 +- .../CredentialStatusListRepository.scala | 47 ++- .../core/service/CredentialServiceImpl.scala | 72 +++-- .../service/CredentialStatusListService.scala | 6 +- .../CredentialStatusListServiceImpl.scala | 9 +- .../core/service/PresentationService.scala | 2 +- .../service/PresentationServiceImpl.scala | 49 +++- .../service/PresentationServiceNotifier.scala | 3 +- ...edentialStatusListRepositoryInMemory.scala | 182 ++++++------ .../service/CredentialServiceSpecHelper.scala | 4 +- .../service/MockPresentationService.scala | 5 +- .../PresentationServiceSpecHelper.scala | 6 +- .../JdbcCredentialStatusListRepository.scala | 213 +++++++------- .../JdbcPresentationRepository.scala | 1 - .../shared/messaging/MessagingService.scala | 106 +++++++ .../messaging/MessagingServiceConfig.scala | 58 ++++ .../identus/shared/messaging/Serde.scala | 55 ++++ .../messaging/WalletIdAndRecordId.scala | 20 ++ .../kafka/InMemoryMessagingService.scala | 146 ++++++++++ .../kafka/ZKafkaMessagingServiceImpl.scala | 136 +++++++++ .../src/test/resources/containers/agent.yml | 94 ++++++ 55 files changed, 2031 insertions(+), 714 deletions(-) create mode 100644 cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql create mode 100644 connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala create mode 100644 event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala create mode 100644 event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala create mode 100644 infrastructure/shared/docker-compose-with-kafka.yml create mode 100644 infrastructure/shared/nginx/nginx.conf create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala diff --git a/build.sbt b/build.sbt index ca50625deb..0df784ff60 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,8 @@ inThisBuild( // scalacOptions += "-Yexplicit-nulls", // scalacOptions += "-Ysafe-init", // scalacOptions += "-Werror", // <=> "-Xfatal-warnings" - scalacOptions += "-Dquill.macro.log=false", // disable quill macro logs // TODO https://github.com/zio/zio-protoquill/issues/470 + scalacOptions += "-Dquill.macro.log=false", // disable quill macro logs // TODO https://github.com/zio/zio-protoquill/issues/470, + scalacOptions ++= Seq("-Xmax-inlines", "50") // manually increase max-inlines above 32 (https://github.com/circe/circe/issues/2162) ) ) @@ -53,6 +54,7 @@ lazy val V = new { val zioCatsInterop = "3.3.0" // TODO "23.1.0.2" // https://mvnrepository.com/artifact/dev.zio/zio-interop-cats val zioMetricsConnector = "2.3.1" val zioMock = "1.0.0-RC12" + val zioKafka = "2.7.5" val mockito = "3.2.18.0" val monocle = "3.2.0" @@ -102,7 +104,11 @@ lazy val D = new { val zioLog: ModuleID = "dev.zio" %% "zio-logging" % V.zioLogging val zioSLF4J: ModuleID = "dev.zio" %% "zio-logging-slf4j" % V.zioLogging val zioJson: ModuleID = "dev.zio" %% "zio-json" % V.zioJson + val zioConcurrent: ModuleID = "dev.zio" %% "zio-concurrent" % V.zio val zioHttp: ModuleID = "dev.zio" %% "zio-http" % V.zioHttp + val zioKafka: ModuleID = "dev.zio" %% "zio-kafka" % V.zioKafka excludeAll ( + ExclusionRule("dev.zio", "zio_3"), ExclusionRule("dev.zio", "zio-streams_3") + ) val zioCatsInterop: ModuleID = "dev.zio" %% "zio-interop-cats" % V.zioCatsInterop val zioMetricsConnectorMicrometer: ModuleID = "dev.zio" %% "zio-metrics-connectors-micrometer" % V.zioMetricsConnector val tapirPrometheusMetrics: ModuleID = "com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % V.tapir @@ -185,7 +191,9 @@ lazy val D_Shared = new { D.typesafeConfig, D.scalaPbGrpc, D.zio, + D.zioConcurrent, D.zioHttp, + D.zioKafka, D.scalaUri, D.zioPrelude, // FIXME: split shared DB stuff as subproject? @@ -341,12 +349,11 @@ lazy val D_Pollux_VC_JWT = new { lazy val D_EventNotification = new { val zio = "dev.zio" %% "zio" % V.zio - val zioConcurrent = "dev.zio" %% "zio-concurrent" % V.zio val zioTest = "dev.zio" %% "zio-test" % V.zio % Test val zioTestSbt = "dev.zio" %% "zio-test-sbt" % V.zio % Test val zioTestMagnolia = "dev.zio" %% "zio-test-magnolia" % V.zio % Test - val zioDependencies: Seq[ModuleID] = Seq(zio, zioConcurrent, zioTest, zioTestSbt, zioTestMagnolia) + val zioDependencies: Seq[ModuleID] = Seq(zio, zioTest, zioTestSbt, zioTestMagnolia) val baseDependencies: Seq[ModuleID] = zioDependencies } diff --git a/cloud-agent/service/server/src/main/resources/application.conf b/cloud-agent/service/server/src/main/resources/application.conf index 13f2a0b4bb..7b5bebb01a 100644 --- a/cloud-agent/service/server/src/main/resources/application.conf +++ b/cloud-agent/service/server/src/main/resources/application.conf @@ -34,22 +34,10 @@ pollux { publicEndpointUrl = "http://localhost:"${agent.httpEndpoint.http.port} publicEndpointUrl = ${?POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL} } - issueBgJobRecordsLimit = 25 - issueBgJobRecordsLimit = ${?ISSUE_BG_JOB_RECORDS_LIMIT} - issueBgJobRecurrenceDelay = 2 seconds - issueBgJobRecurrenceDelay = ${?ISSUE_BG_JOB_RECURRENCE_DELAY} - issueBgJobProcessingParallelism = 5 - issueBgJobProcessingParallelism = ${?ISSUE_BG_JOB_PROCESSING_PARALLELISM} - presentationBgJobRecordsLimit = 25 - presentationBgJobRecordsLimit = ${?PRESENTATION_BG_JOB_RECORDS_LIMIT} - presentationBgJobRecurrenceDelay = 2 seconds - presentationBgJobRecurrenceDelay = ${?PRESENTATION_BG_JOB_RECURRENCE_DELAY} - presentationBgJobProcessingParallelism = 5 - presentationBgJobProcessingParallelism = ${?PRESENTATION_BG_JOB_PROCESSING_PARALLELISM} - syncRevocationStatusesBgJobRecurrenceDelay = 2 seconds - syncRevocationStatusesBgJobRecurrenceDelay = ${?SYNC_REVOCATION_STATUSES_BG_JOB_RECURRENCE_DELAY} - syncRevocationStatusesBgJobProcessingParallelism = 5 - syncRevocationStatusesBgJobProcessingParallelism = ${?SYNC_REVOCATION_STATUSES_BG_JOB_PROCESSING_PARALLELISM} + statusListSyncTriggerRecurrenceDelay = 30 seconds + statusListSyncTriggerRecurrenceDelay = ${?STATUS_LIST_SYNC_TRIGGER_RECURRENCE_DELAY} + didStateSyncTriggerRecurrenceDelay = 30 seconds + didStateSyncTriggerRecurrenceDelay = ${?DID_STATE_SYNC_TRIGGER_RECURRENCE_DELAY} credential.sdJwt.expiry = 30 days credential.sdJwt.expiry = ${?CREDENTIAL_SD_JWT_EXPIRY} presentationInvitationExpiry = 300 seconds @@ -81,8 +69,6 @@ connect { connectBgJobRecordsLimit = ${?CONNECT_BG_JOB_RECORDS_LIMIT} connectBgJobRecurrenceDelay = 2 seconds connectBgJobRecurrenceDelay = ${?CONNECT_BG_JOB_RECURRENCE_DELAY} - connectBgJobProcessingParallelism = 5 - connectBgJobProcessingParallelism = ${?CONNECT_BG_JOB_PROCESSING_PARALLELISM} connectInvitationExpiry = 300 seconds connectInvitationExpiry = ${?CONNECT_INVITATION_EXPIRY} } @@ -262,4 +248,49 @@ agent { authApiKey = "default" authApiKey = ${?DEFAULT_WALLET_AUTH_API_KEY} } + messagingService { + connectFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + issueFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + presentFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + didStateSync { + consumerCount = 5 + } + statusListSync { + consumerCount = 5 + } + inMemoryQueueCapacity = 1000 + kafkaEnabled = false + kafkaEnabled = ${?DEFAULT_KAFKA_ENABLED} + kafka { + bootstrapServers = "kafka:9092" + consumers { + autoCreateTopics = false, + maxPollRecords = 500 + maxPollInterval = 5.minutes + pollTimeout = 50.millis + rebalanceSafeCommits = true + } + } + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala index d3af09cf2b..d010d4c5f6 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala @@ -5,12 +5,9 @@ import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} import org.hyperledger.identus.agent.server.jobs.* import org.hyperledger.identus.agent.walletapi.model.{Entity, Wallet, WalletSeed} -import org.hyperledger.identus.agent.walletapi.service.{EntityService, ManagedDIDService, WalletManagementService} -import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage +import org.hyperledger.identus.agent.walletapi.service.{EntityService, WalletManagementService} import org.hyperledger.identus.castor.controller.{DIDRegistrarServerEndpoints, DIDServerEndpoints} -import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.connect.controller.ConnectionServerEndpoints -import org.hyperledger.identus.connect.core.service.ConnectionService import org.hyperledger.identus.credentialstatus.controller.CredentialStatusServiceEndpoints import org.hyperledger.identus.event.controller.EventServerEndpoints import org.hyperledger.identus.event.notification.EventNotificationConfig @@ -18,108 +15,35 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyAuthenticator import org.hyperledger.identus.iam.entity.http.EntityServerEndpoints import org.hyperledger.identus.iam.wallet.http.WalletManagementServerEndpoints import org.hyperledger.identus.issue.controller.IssueServerEndpoints -import org.hyperledger.identus.mercury.{DidOps, HttpClient} import org.hyperledger.identus.oid4vci.CredentialIssuerServerEndpoints -import org.hyperledger.identus.pollux.core.service.{CredentialService, PresentationService} import org.hyperledger.identus.pollux.credentialdefinition.CredentialDefinitionRegistryServerEndpoints import org.hyperledger.identus.pollux.credentialschema.{ SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints } import org.hyperledger.identus.pollux.prex.PresentationExchangeServerEndpoints -import org.hyperledger.identus.pollux.vc.jwt.DidResolver as JwtDidResolver import org.hyperledger.identus.presentproof.controller.PresentProofServerEndpoints -import org.hyperledger.identus.resolvers.DIDResolver -import org.hyperledger.identus.shared.http.UriResolver -import org.hyperledger.identus.shared.models.{HexString, WalletAccessContext, WalletAdministrationContext, WalletId} -import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds +import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.system.controller.SystemServerEndpoints import org.hyperledger.identus.verification.controller.VcVerificationServerEndpoints import zio.* -import zio.metrics.* - object CloudAgentApp { def run = for { _ <- AgentInitialization.run - _ <- issueCredentialDidCommExchangesJob.debug.fork - _ <- presentProofExchangeJob.debug.fork - _ <- connectDidCommExchangesJob.debug.fork - _ <- syncDIDPublicationStateFromDltJob.debug.fork - _ <- syncRevocationStatusListsJob.debug.fork + _ <- ConnectBackgroundJobs.connectFlowsHandler + _ <- IssueBackgroundJobs.issueFlowsHandler + _ <- PresentBackgroundJobs.presentFlowsHandler + _ <- DIDStateSyncBackgroundJobs.didStateSyncTrigger + _ <- DIDStateSyncBackgroundJobs.didStateSyncHandler + _ <- StatusListJobs.statusListsSyncTrigger + _ <- StatusListJobs.statusListSyncHandler _ <- AgentHttpServer.run.tapDefect(e => ZIO.logErrorCause("Agent HTTP Server failure", e)).fork fiber <- DidCommHttpServer.run.tapDefect(e => ZIO.logErrorCause("DIDComm HTTP Server failure", e)).fork _ <- WebhookPublisher.layer.build.map(_.get[WebhookPublisher]).flatMap(_.run.fork) _ <- fiber.join *> ZIO.log(s"Server End") _ <- ZIO.never } yield () - - private val issueCredentialDidCommExchangesJob: RIO[ - AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & DIDNonSecretStorage & - DIDService & ManagedDIDService & PresentationService & WalletManagementService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (IssueBackgroundJobs.issueCredentialDidCommExchanges @@ Metric - .gauge("issuance_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.issueBgJobRecurrenceDelay)) - .unit - } yield () - - private val presentProofExchangeJob: RIO[ - AppConfig & DidOps & UriResolver & DIDResolver & JwtDidResolver & HttpClient & PresentationService & - CredentialService & DIDNonSecretStorage & DIDService & ManagedDIDService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (PresentBackgroundJobs.presentProofExchanges @@ Metric - .gauge("present_proof_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.presentationBgJobRecurrenceDelay)) - .unit - } yield () - - private val connectDidCommExchangesJob: RIO[ - AppConfig & DidOps & DIDResolver & HttpClient & ConnectionService & ManagedDIDService & DIDNonSecretStorage & - WalletManagementService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (ConnectBackgroundJobs.didCommExchanges @@ Metric - .gauge("connection_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.connect.connectBgJobRecurrenceDelay)) - .unit - } yield () - - private val syncRevocationStatusListsJob = { - for { - config <- ZIO.service[AppConfig] - _ <- (StatusListJobs.syncRevocationStatuses @@ Metric - .gauge("revocation_status_list_sync_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.syncRevocationStatusesBgJobRecurrenceDelay)) - } yield () - } - - private val syncDIDPublicationStateFromDltJob: URIO[ManagedDIDService & WalletManagementService, Unit] = - ZIO - .serviceWithZIO[WalletManagementService](_.listWallets().map(_._1)) - .flatMap { wallets => - ZIO.foreach(wallets) { wallet => - DIDStateSyncBackgroundJobs.syncDIDPublicationStateFromDlt - .provideSomeLayer(ZLayer.succeed(WalletAccessContext(wallet.id))) - } - } - .catchAll(e => ZIO.logError(s"error while syncing DID publication state: $e")) - .repeat(Schedule.spaced(10.seconds)) - .unit - .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) - } object AgentHttpServer { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala index 9f163a0a11..922593389a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala @@ -7,7 +7,6 @@ import org.hyperledger.identus.agent.server.http.ZioHttpClient import org.hyperledger.identus.agent.server.sql.Migrations as AgentMigrations import org.hyperledger.identus.agent.walletapi.service.{ EntityServiceImpl, - ManagedDIDService, ManagedDIDServiceWithEventNotificationImpl, WalletManagementServiceImpl } @@ -16,7 +15,6 @@ import org.hyperledger.identus.agent.walletapi.sql.{ JdbcEntityRepository, JdbcWalletNonSecretStorage } -import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} import org.hyperledger.identus.castor.core.model.did.{ Service as DidDocumentService, @@ -36,7 +34,7 @@ import org.hyperledger.identus.iam.authentication.{DefaultAuthenticator, Oid4vci import org.hyperledger.identus.iam.authentication.apikey.JdbcAuthenticationRepository import org.hyperledger.identus.iam.authorization.core.EntityPermissionManagementService import org.hyperledger.identus.iam.authorization.DefaultPermissionManagementService -import org.hyperledger.identus.iam.entity.http.controller.{EntityController, EntityControllerImpl} +import org.hyperledger.identus.iam.entity.http.controller.EntityControllerImpl import org.hyperledger.identus.iam.wallet.http.controller.WalletManagementControllerImpl import org.hyperledger.identus.issue.controller.IssueControllerImpl import org.hyperledger.identus.mercury.* @@ -47,7 +45,6 @@ import org.hyperledger.identus.pollux.core.service.* import org.hyperledger.identus.pollux.core.service.verification.VcVerificationServiceImpl import org.hyperledger.identus.pollux.credentialdefinition.controller.CredentialDefinitionControllerImpl import org.hyperledger.identus.pollux.credentialschema.controller.{ - CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl } @@ -66,6 +63,9 @@ import org.hyperledger.identus.pollux.sql.repository.{ } import org.hyperledger.identus.presentproof.controller.PresentProofControllerImpl import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId +import org.hyperledger.identus.shared.models.WalletId import org.hyperledger.identus.system.controller.SystemControllerImpl import org.hyperledger.identus.verification.controller.VcVerificationControllerImpl import zio.* @@ -77,6 +77,7 @@ import zio.metrics.connectors.micrometer.MicrometerConfig import zio.metrics.jvm.DefaultJvmMetrics import java.security.Security +import java.util.UUID object MainApp extends ZIOAppDefault { @@ -167,7 +168,6 @@ object MainApp extends ZIOAppDefault { ) _ <- preMigrations _ <- migrations - app <- CloudAgentApp.run .provide( DidCommX.liveLayer, @@ -252,6 +252,11 @@ object MainApp extends ZIOAppDefault { // HTTP client SystemModule.zioHttpClientLayer, Scope.default, + // Messaging Service + ZLayer.fromZIO(ZIO.service[AppConfig].map(_.agent.messagingService)), + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId], + messaging.MessagingService.producerLayer[WalletId, WalletId] ) } yield app diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala index 0f75561812..364ff510bc 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala @@ -4,7 +4,7 @@ import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.iam.authentication.AuthenticationConfig import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.db.DbConfig -import zio.config.* +import org.hyperledger.identus.shared.messaging.MessagingServiceConfig import zio.config.magnolia.* import zio.Config @@ -70,22 +70,13 @@ final case class PolluxConfig( database: DatabaseConfig, credentialSdJwtExpirationTime: Duration, statusListRegistry: StatusListRegistryConfig, - issueBgJobRecordsLimit: Int, - issueBgJobRecurrenceDelay: Duration, - issueBgJobProcessingParallelism: Int, - presentationBgJobRecordsLimit: Int, - presentationBgJobRecurrenceDelay: Duration, - presentationBgJobProcessingParallelism: Int, - syncRevocationStatusesBgJobRecurrenceDelay: Duration, - syncRevocationStatusesBgJobProcessingParallelism: Int, + statusListSyncTriggerRecurrenceDelay: Duration, + didStateSyncTriggerRecurrenceDelay: Duration, presentationInvitationExpiry: Duration, issuanceInvitationExpiry: Duration, ) final case class ConnectConfig( database: DatabaseConfig, - connectBgJobRecordsLimit: Int, - connectBgJobRecurrenceDelay: Duration, - connectBgJobProcessingParallelism: Int, connectInvitationExpiry: Duration, ) @@ -173,7 +164,8 @@ final case class AgentConfig( verification: VerificationConfig, secretStorage: SecretStorageConfig, webhookPublisher: WebhookPublisherConfig, - defaultWallet: DefaultWalletConfig + defaultWallet: DefaultWalletConfig, + messagingService: MessagingServiceConfig ) { def validate: Either[String, Unit] = for { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala index 0d73369e82..44ffa1cea8 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala @@ -1,9 +1,11 @@ package org.hyperledger.identus.agent.server.http +import org.http4s.{MediaType, Request, Response, Status} +import org.http4s.headers.`Content-Type` +import org.http4s.server.ServiceErrorHandler import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.shared.models.{Failure, StatusCode, UnmanagedFailureException} import org.log4s.* -import sttp.tapir.* import sttp.tapir.json.zio.jsonBody import sttp.tapir.server.interceptor.* import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} @@ -11,6 +13,7 @@ import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.F import sttp.tapir.server.interceptor.exception.ExceptionHandler import sttp.tapir.server.interceptor.reject.RejectHandler import sttp.tapir.server.model.ValuedEndpointOutput +import zio.{Task, ZIO} import scala.language.implicitConversions @@ -19,7 +22,7 @@ object CustomServerInterceptors { private val logger: Logger = getLogger private val endpointOutput = jsonBody[ErrorResponse] - private def defectHandler(response: ErrorResponse, maybeCause: Option[Throwable] = None) = { + private def tapirDefectHandler(response: ErrorResponse, maybeCause: Option[Throwable] = None) = { val statusCode = sttp.model.StatusCode(response.status) // Log defect as 'error' when status code matches a server error (5xx). Log other defects as 'debug'. (statusCode, maybeCause) match @@ -27,39 +30,43 @@ object CustomServerInterceptors { case (sc, None) if sc.isServerError => logger.error(endpointOutput.codec.encode(response)) case (_, Some(cause)) => logger.debug(cause)(endpointOutput.codec.encode(response)) case (_, None) => logger.debug(endpointOutput.codec.encode(response)) - Some(ValuedEndpointOutput(endpointOutput, response).prepend(sttp.tapir.statusCode, statusCode)) + ValuedEndpointOutput(endpointOutput, response).prepend(sttp.tapir.statusCode, statusCode) } - def exceptionHandler[F[_]]: ExceptionHandler[F] = ExceptionHandler.pure[F](ctx => + def tapirExceptionHandler[F[_]]: ExceptionHandler[F] = ExceptionHandler.pure[F](ctx => ctx.e match - case UnmanagedFailureException(failure: Failure) => defectHandler(failure) + case UnmanagedFailureException(failure: Failure) => Some(tapirDefectHandler(failure)) case e => - defectHandler( - ErrorResponse( - StatusCode.InternalServerError.code, - s"error:InternalServerError", - "Internal Server Error", - Some( - s"An unexpected error occurred when processing the request: " + - s"path=['${ctx.request.showShort}']" - ) - ), - Some(ctx.e) + Some( + tapirDefectHandler( + ErrorResponse( + StatusCode.InternalServerError.code, + s"error:InternalServerError", + "Internal Server Error", + Some( + s"An unexpected error occurred when processing the request: " + + s"path=['${ctx.request.showShort}']" + ) + ), + Some(ctx.e) + ) ) ) - def rejectHandler[F[_]]: RejectHandler[F] = RejectHandler.pure[F](resultFailure => - defectHandler( - ErrorResponse( - StatusCode.NotFound.code, - s"error:ResourcePathNotFound", - "Resource Path Not Found", - Some(s"The requested resource path doesn't exist.") + def tapirRejectHandler[F[_]]: RejectHandler[F] = RejectHandler.pure[F](resultFailure => + Some( + tapirDefectHandler( + ErrorResponse( + StatusCode.NotFound.code, + s"error:ResourcePathNotFound", + "Resource Path Not Found", + Some(s"The requested resource path doesn't exist.") + ) ) ) ) - def decodeFailureHandler: DecodeFailureHandler = (ctx: DecodeFailureContext) => { + def tapirDecodeFailureHandler: DecodeFailureHandler = (ctx: DecodeFailureContext) => { /** As per the Tapir Decode Failures documentation: * @@ -79,17 +86,39 @@ object CustomServerInterceptors { DefaultDecodeFailureHandler.respond(ctx) match case Some((sc, _)) => val details = FailureMessages.failureMessage(ctx) - defectHandler( - ErrorResponse( - sc.code, - s"error:RequestBodyDecodingFailure", - "Request Body Decoding Failure", - Some( - s"An error occurred when decoding the request body: " + - s"path=['${ctx.request.showShort}'], details=[$details]" + Some( + tapirDefectHandler( + ErrorResponse( + sc.code, + s"error:RequestBodyDecodingFailure", + "Request Body Decoding Failure", + Some( + s"An error occurred when decoding the request body: " + + s"path=['${ctx.request.showShort}'], details=[$details]" + ) ) ) ) case None => None } + + def http4sServiceErrorHandler: ServiceErrorHandler[Task] = (req: Request[Task]) => { case t: Throwable => + val res = tapirDefectHandler( + ErrorResponse( + StatusCode.InternalServerError.code, + s"error:InternalServerError", + "Internal Server Error", + Some( + s"An unexpected error occurred when servicing the request: " + + s"path=['${req.method.name} ${req.uri.copy(scheme = None, authority = None, fragment = None).toString}']" + ) + ), + Some(t) + ) + ZIO.succeed( + Response(Status.InternalServerError) + .withEntity(endpointOutput.codec.encode(res.value._2)) + .withContentType(`Content-Type`(MediaType.application.json)) + ) + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala index 05d56eb62b..1293185891 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala @@ -93,9 +93,9 @@ class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry, metricsNam options <- ZIO.attempt { Http4sServerOptions .customiseInterceptors[Task] - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) .serverLog(None) .metricsInterceptor( srv.metricsInterceptor( @@ -123,6 +123,7 @@ class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry, metricsNam ZIO.executor.flatMap(executor => BlazeServerBuilder[Task] .withExecutionContext(executor.asExecutionContext) + .withServiceErrorHandler(CustomServerInterceptors.http4sServiceErrorHandler) .bindHttp(port, "0.0.0.0") .withHttpApp(Router("/" -> http4sEndpoints).orNotFound) .serve diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala index 1708ca1517..67867bb2fb 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala @@ -19,7 +19,6 @@ import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.pollux.core.model.error.{CredentialServiceError, PresentationError} import org.hyperledger.identus.pollux.core.model.DidCommID import org.hyperledger.identus.pollux.core.service.CredentialService -import org.hyperledger.identus.pollux.sdjwt.SDJWT.* import org.hyperledger.identus.pollux.vc.jwt.{ DIDResolutionFailed, DIDResolutionSucceeded, @@ -29,8 +28,11 @@ import org.hyperledger.identus.pollux.vc.jwt.{ * } import org.hyperledger.identus.shared.crypto.* +import org.hyperledger.identus.shared.messaging.ConsumerJobConfig +import org.hyperledger.identus.shared.messaging.MessagingService.RetryStep import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext} -import zio.{ZIO, ZLayer} +import zio.{durationInt, Duration, ZIO, ZLayer} +import zio.prelude.OrdOps import java.time.Instant import java.util.Base64 @@ -229,4 +231,20 @@ trait BackgroundJobsHelper { case _ => ZIO.unit } } + + def retryStepsFromConfig(topicName: String, jobConfig: ConsumerJobConfig): Seq[RetryStep] = { + val retryTopics = jobConfig.retryStrategy match + case None => Seq.empty + case Some(rs) => + (1 to rs.maxRetries).map(i => + ( + s"$topicName-retry-$i", + rs.initialDelay.multipliedBy(Math.pow(2, i - 1).toLong).min(rs.maxDelay) + ) + ) + val topics = retryTopics prepended (topicName, 0.seconds) appended (s"$topicName-DLQ", Duration.Infinity) + (0 until topics.size - 1).map { i => + RetryStep(topics(i)._1, jobConfig.consumerCount, topics(i)._2, topics(i + 1)._1) + } + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala index 46335fd059..07cfd05a22 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala @@ -2,49 +2,63 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.jobs.BackgroundJobError.ErrorResponseReceivedFromPeerAgent -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.{KeyNotFoundError, WalletNotFoundError} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage -import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.{ - InvalidStateForOperation, - RecordIdNotFound -} import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.service.ConnectionService import org.hyperledger.identus.mercury.* -import org.hyperledger.identus.mercury.model.error.SendMessageError import org.hyperledger.identus.resolvers.DIDResolver -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.* +import java.util.UUID + object ConnectBackgroundJobs extends BackgroundJobsHelper { - val didCommExchanges = { - for { - connectionService <- ZIO.service[ConnectionService] - config <- ZIO.service[AppConfig] - records <- connectionService - .findRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.connect.connectBgJobRecordsLimit, - ConnectionRecord.ProtocolState.ConnectionRequestPending, - ConnectionRecord.ProtocolState.ConnectionResponsePending - ) - _ <- ZIO.foreachPar(records)(performExchange).withParallelism(config.connect.connectBgJobProcessingParallelism) - } yield () - } + private val TOPIC_NAME = "connect" + + val connectFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + ConnectBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.connectFlow) + ) + } yield () - private def performExchange( - record: ConnectionRecord - ): URIO[ + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ DidOps & DIDResolver & HttpClient & ConnectionService & ManagedDIDService & DIDNonSecretStorage & AppConfig, Unit - ] = { + ] = + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") + connectionService <- ZIO.service[ConnectionService] + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- connectionService + .findRecordById(message.value.recordId) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage("Record Not Found")) + _ <- performExchange(record) + .tapSomeError { case (walletAccessContext, errorResponse) => + for { + connectService <- ZIO.service[ConnectionService] + _ <- connectService + .reportProcessingFailure(record.id, Some(errorResponse)) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("connection_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + + private def performExchange(record: ConnectionRecord) = { import ProtocolState.* import Role.* @@ -179,26 +193,10 @@ object ConnectBackgroundJobs extends BackgroundJobsHelper { @@ Metric .gauge("connection_flow_inviter_process_connection_record_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - case _ => ZIO.unit + case r => ZIO.logWarning(s"Invalid candidate record received for processing: $r") *> ZIO.unit } exchange - .tapError({ - case walletNotFound: WalletNotFoundError => - ZIO.logErrorCause( - s"Connect - Error processing record: ${record.id}", - Cause.fail(walletNotFound) - ) - case ((walletAccessContext, errorResponse)) => - for { - connectService <- ZIO.service[ConnectionService] - _ <- connectService - .reportProcessingFailure(record.id, Some(errorResponse)) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - } yield () - }) - .catchAll(e => ZIO.logErrorCause(s"Connect - Error processing record: ${record.id} ", Cause.fail(e))) - .catchAllDefect(d => ZIO.logErrorCause(s"Connect - Defect processing record: ${record.id}", Cause.fail(d))) } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala index 5d4ff494ea..2ee44e91bc 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala @@ -1,17 +1,54 @@ package org.hyperledger.identus.agent.server.jobs -import org.hyperledger.identus.agent.walletapi.model.error.GetManagedDIDError -import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.agent.server.config.AppConfig +import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, WalletManagementService} +import org.hyperledger.identus.shared.messaging.{Message, MessagingService, Producer} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext, WalletId} +import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* +import zio.metrics.Metric -object DIDStateSyncBackgroundJobs { +object DIDStateSyncBackgroundJobs extends BackgroundJobsHelper { - val syncDIDPublicationStateFromDlt: ZIO[WalletAccessContext with ManagedDIDService, GetManagedDIDError, Unit] = - for { + private val TOPIC_NAME = "sync-did-state" + + val didStateSyncTrigger = { + (for { + config <- ZIO.service[AppConfig] + producer <- ZIO.service[Producer[WalletId, WalletId]] + trigger = for { + walletManagementService <- ZIO.service[WalletManagementService] + wallets <- walletManagementService.listWallets().map(_._1) + _ <- ZIO.logInfo(s"Triggering DID state sync for '${wallets.size}' wallets") + _ <- ZIO.foreach(wallets)(w => producer.produce(TOPIC_NAME, w.id, w.id)) + } yield () + _ <- trigger + .catchAll(e => ZIO.logError(s"error while syncing DID publication state: $e")) + .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) + .repeat(Schedule.spaced(config.pollux.didStateSyncTriggerRecurrenceDelay)) + } yield ()).debug.fork + } + + val didStateSyncHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + DIDStateSyncBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.didStateSync) + ) + } yield () + + private def handleMessage(message: Message[WalletId, WalletId]): RIO[ManagedDIDService, Unit] = { + val effect = for { managedDidService <- ZIO.service[ManagedDIDService] _ <- managedDidService.syncManagedDIDState _ <- managedDidService.syncUnconfirmedUpdateOperations } yield () - + effect + .provideSomeLayer(ZLayer.succeed(WalletAccessContext(message.value))) + .catchAll(t => ZIO.logErrorCause("Unable to syncing DID publication state", Cause.fail(t))) + @@ Metric + .gauge("did_publication_state_sync_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala index ffc617df3c..3cdde2853f 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala @@ -2,40 +2,61 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.jobs.BackgroundJobError.ErrorResponseReceivedFromPeerAgent -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.WalletNotFoundError -import org.hyperledger.identus.castor.core.model.did.* +import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService +import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage import org.hyperledger.identus.mercury.* -import org.hyperledger.identus.mercury.protocol.issuecredential.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.service.CredentialService -import org.hyperledger.identus.shared.models.Failure +import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.* +import java.util.UUID + object IssueBackgroundJobs extends BackgroundJobsHelper { - val issueCredentialDidCommExchanges = { - for { + private val TOPIC_NAME = "issue" + + val issueFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + IssueBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.issueFlow) + ) + } yield () + + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + HttpClient & DidOps & DIDResolver & (CredentialService & DIDNonSecretStorage & (ManagedDIDService & AppConfig)), + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") credentialService <- ZIO.service[CredentialService] - config <- ZIO.service[AppConfig] - records <- credentialService - .getIssueCredentialRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.pollux.issueBgJobRecordsLimit, - IssueCredentialRecord.ProtocolState.OfferPending, - IssueCredentialRecord.ProtocolState.RequestPending, - IssueCredentialRecord.ProtocolState.RequestGenerated, - IssueCredentialRecord.ProtocolState.RequestReceived, - IssueCredentialRecord.ProtocolState.CredentialPending, - IssueCredentialRecord.ProtocolState.CredentialGenerated - ) - _ <- ZIO - .foreachPar(records)(performIssueCredentialExchange) - .withParallelism(config.pollux.issueBgJobProcessingParallelism) - } yield () + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- credentialService + .findById(DidCommID(message.value.recordId.toString)) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage(s"Record Not Found: ${message.value.recordId}")) + _ <- performIssueCredentialExchange(record) + .tapSomeError { case (walletAccessContext, errorResponse) => + for { + credentialService <- ZIO.service[CredentialService] + _ <- credentialService + .reportProcessingFailure(record.id, Some(errorResponse)) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("issuance_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) } private def counterMetric(key: String) = Metric @@ -136,7 +157,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { "issuance_flow_issuer_send_credential_msg_succeed_counter" ) - val aux = for { + val exchange = for { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { // Offer should be sent from Issuer to Holder @@ -227,8 +248,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] _ <- credentialService @@ -273,8 +294,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] _ <- credentialService @@ -319,8 +340,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] @@ -629,33 +650,12 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { @@ IssuerSendCredentialAll @@ Metric .gauge("issuance_flow_issuer_send_cred_flow_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - - case record: IssueCredentialRecord => - ZIO.logDebug(s"IssuanceRecord: ${record.id} - ${record.protocolState}") *> ZIO.unit + case r: IssueCredentialRecord => + ZIO.logWarning(s"Invalid candidate record received for processing: $r") *> ZIO.unit } } yield () - aux - .tapError( - { - case walletNotFound: WalletNotFoundError => ZIO.unit - case CredentialServiceError.RecordNotFound(_, _) => ZIO.unit - case CredentialServiceError.UnsupportedDidFormat(_) => ZIO.unit - case failure: Failure => ZIO.unit - case ((walletAccessContext, failure)) => - for { - credentialService <- ZIO.service[CredentialService] - _ <- credentialService - .reportProcessingFailure(record.id, Some(failure)) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - } yield () - } - ) - .catchAll(e => ZIO.logErrorCause(s"Issue Credential - Error processing record: ${record.id} ", Cause.fail(e))) - .catchAllDefect(d => - ZIO.logErrorCause(s"Issue Credential - Defect processing record: ${record.id}", Cause.fail(d)) - ) - + exchange } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala index 9938b6b50b..4bfb247176 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala @@ -30,17 +30,18 @@ import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, IssuerPublicKey, import org.hyperledger.identus.pollux.vc.jwt.{DidResolver as JwtDidResolver, Issuer as JwtIssuer, JWT, JwtPresentation} import org.hyperledger.identus.resolvers.DIDResolver import org.hyperledger.identus.shared.http.* -import org.hyperledger.identus.shared.models.* +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{Failure, *} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* -import zio.json.* -import zio.json.ast.Json import zio.metrics.* import zio.prelude.Validation import zio.prelude.ZValidation.{Failure as ZFailure, *} -import java.time.{Clock, Instant, ZoneId} +import java.time.{Instant, ZoneId} +import java.util.UUID object PresentBackgroundJobs extends BackgroundJobsHelper { @@ -55,47 +56,50 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { private type MESSAGING_RESOURCES = DidOps & DIDResolver & HttpClient - val presentProofExchanges: ZIO[RESOURCES, Throwable, Unit] = { - for { + private val TOPIC_NAME = "present" + + val presentFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + PresentBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.presentFlow) + ) + } yield () + + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + RESOURCES, + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Present Proof Handling recordId: ${message.value} via Kafka queue") presentationService <- ZIO.service[PresentationService] - config <- ZIO.service[AppConfig] - records <- presentationService - .getPresentationRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.pollux.presentationBgJobRecordsLimit, - PresentationRecord.ProtocolState.RequestPending, - PresentationRecord.ProtocolState.PresentationPending, - PresentationRecord.ProtocolState.PresentationGenerated, - PresentationRecord.ProtocolState.PresentationReceived - ) - .mapError(err => Throwable(s"Error occurred while getting Presentation records: $err")) - _ <- ZIO.logInfo(s"Processing ${records.size} Presentation records") - _ <- ZIO - .foreachPar(records)(performPresentProofExchange) - .withParallelism(config.pollux.presentationBgJobProcessingParallelism) - } yield () + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- presentationService + .findPresentationRecord(DidCommID(message.value.recordId.toString)) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage("Record Not Found")) + _ <- performPresentProofExchange(record) + .tapSomeError { case f: Failure => + for { + presentationService <- ZIO.service[PresentationService] + _ <- presentationService + .reportProcessingFailure(record.id, Some(f)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("present_proof_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) } private def counterMetric(key: String) = Metric .counterInt(key) .fromConst(1) - private def performPresentProofExchange(record: PresentationRecord): URIO[RESOURCES, Unit] = - aux(record) - .catchAll { - case ex: Failure => - ZIO - .service[PresentationService] - .flatMap(_.reportProcessingFailure(record.id, Some(ex))) - case ex => ZIO.logErrorCause(s"PresentBackgroundJobs - Error processing record: ${record.id}", Cause.fail(ex)) - } - .catchAllDefect(d => - ZIO.logErrorCause(s"PresentBackgroundJobs - Defect processing record: ${record.id}", Cause.fail(d)) - ) - - private def aux(record: PresentationRecord): ZIO[RESOURCES, ERROR, Unit] = { + private def performPresentProofExchange(record: PresentationRecord): ZIO[RESOURCES, ERROR, Unit] = { import org.hyperledger.identus.pollux.core.model.PresentationRecord.ProtocolState.* - for { + val exchange = for { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { case PresentationRecord( @@ -604,6 +608,8 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { ZIO.logWarning(s"Unhandled PresentationRecord state: ${record.protocolState}") } } yield () + + exchange } object Prover { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala index 71d02db3e2..1fe5d77551 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala @@ -1,131 +1,181 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig +import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.castor.core.model.did.VerificationRelationship +import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.mercury.* import org.hyperledger.identus.mercury.protocol.revocationnotificaiton.RevocationNotification +import org.hyperledger.identus.pollux.core.model.{CredInStatusList, CredentialStatusListWithCreds} import org.hyperledger.identus.pollux.core.service.{CredentialService, CredentialStatusListService} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{VCStatusList2021, VCStatusList2021Error} -import org.hyperledger.identus.shared.models.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021, VCStatusList2021Error} +import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, Producer, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.Metric +import java.util.UUID + object StatusListJobs extends BackgroundJobsHelper { - val syncRevocationStatuses = - for { - credentialStatusListService <- ZIO.service[CredentialStatusListService] - credentialService <- ZIO.service[CredentialService] - credentialStatusListsWithCreds <- credentialStatusListService.getCredentialsAndItsStatuses - @@ Metric - .gauge("revocation_status_list_sync_get_status_lists_w_creds_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) + private val TOPIC_NAME = "sync-status-list" - updatedVcStatusListsCredsEffects = credentialStatusListsWithCreds.map { statusListWithCreds => - val vcStatusListCredString = statusListWithCreds.statusListCredential - val walletAccessContext = WalletAccessContext(statusListWithCreds.walletId) + val statusListsSyncTrigger = { + (for { + config <- ZIO.service[AppConfig] + producer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]] + trigger = for { + credentialStatusListService <- ZIO.service[CredentialStatusListService] + walletAndStatusListIds <- credentialStatusListService.getCredentialStatusListIds + _ <- ZIO.logInfo(s"Triggering status list revocation sync for '${walletAndStatusListIds.size}' status lists") + _ <- ZIO.foreach(walletAndStatusListIds) { (walletId, statusListId) => + producer.produce(TOPIC_NAME, walletId.toUUID, WalletIdAndRecordId(walletId.toUUID, statusListId)) + } + } yield () + _ <- trigger.repeat(Schedule.spaced(config.pollux.statusListSyncTriggerRecurrenceDelay)) + } yield ()).debug.fork + } - val effect = for { - vcStatusListCredJson <- ZIO - .fromEither(io.circe.parser.parse(vcStatusListCredString)) - .mapError(_.underlying) - issuer <- createJwtVcIssuer( - statusListWithCreds.issuer, - VerificationRelationship.AssertionMethod, - None - ) - vcStatusListCred <- VCStatusList2021 - .decodeFromJson(vcStatusListCredJson, issuer) - .mapError(x => new Throwable(x.msg)) - bitString <- vcStatusListCred.getBitString.mapError(x => new Throwable(x.msg)) - updateBitStringEffects = statusListWithCreds.credentials.map { cred => - if cred.isCanceled then { - val sendMessageEffect = for { - maybeIssueCredentialRecord <- credentialService.findById(cred.issueCredentialRecordId) - issueCredentialRecord <- ZIO - .fromOption(maybeIssueCredentialRecord) - .mapError(_ => - new Throwable(s"Issue credential record not found by id: ${cred.issueCredentialRecordId}") - ) - issueCredentialData <- ZIO - .fromOption(issueCredentialRecord.issueCredentialData) - .mapError(_ => - new Throwable( - s"Issue credential data not found in issue credential record by id: ${cred.issueCredentialRecordId}" - ) - ) - issueCredentialProtocolThreadId <- ZIO - .fromOption(issueCredentialData.thid) - .mapError(_ => new Throwable("thid not found in issue credential data")) - revocationNotification = RevocationNotification.build( - issueCredentialData.from, - issueCredentialData.to, - issueCredentialProtocolThreadId = issueCredentialProtocolThreadId - ) - didCommAgent <- buildDIDCommAgent(issueCredentialData.from) - response <- MessagingService - .send(revocationNotification.makeMessage) - .provideSomeLayer(didCommAgent) @@ Metric - .gauge("revocation_status_list_sync_revocation_notification_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) - } yield response + val statusListSyncHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + StatusListJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.statusListSync) + ) + } yield () - val updateBitStringEffect = bitString.setRevokedInPlace(cred.statusListIndex, true) + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + DIDService & ManagedDIDService & CredentialService & DidOps & DIDResolver & HttpClient & + CredentialStatusListService, + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") + credentialStatusListService <- ZIO.service[CredentialStatusListService] + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + statusListWithCreds <- credentialStatusListService + .getCredentialStatusListWithCreds(message.value.recordId) + .provideSome(ZLayer.succeed(walletAccessContext)) + _ <- updateStatusList(statusListWithCreds) + } yield ()) @@ Metric + .gauge("revocation_status_list_sync_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } - val updateAndNotify = for { - updated <- updateBitStringEffect.mapError(x => new Throwable(x.message)) - _ <- - if !cred.isProcessed then - sendMessageEffect.flatMap { resp => - if (resp.status >= 200 && resp.status < 300) - ZIO.logInfo("successfully sent revocation notification message") - else ZIO.logError(s"failed to send revocation notification message") - } - else ZIO.unit - } yield updated - updateAndNotify.provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ Metric - .gauge("revocation_status_list_sync_process_single_credential_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) - } else ZIO.unit - } - _ <- ZIO - .collectAll(updateBitStringEffects) + private def updateStatusList(statusListWithCreds: CredentialStatusListWithCreds) = { + for { + credentialStatusListService <- ZIO.service[CredentialStatusListService] + vcStatusListCredString = statusListWithCreds.statusListCredential + walletAccessContext = WalletAccessContext(statusListWithCreds.walletId) + effect = for { + vcStatusListCredJson <- ZIO + .fromEither(io.circe.parser.parse(vcStatusListCredString)) + .mapError(_.underlying) + issuer <- createJwtVcIssuer(statusListWithCreds.issuer, VerificationRelationship.AssertionMethod, None) + vcStatusListCred <- VCStatusList2021 + .decodeFromJson(vcStatusListCredJson, issuer) + .mapError(x => new Throwable(x.msg)) + bitString <- vcStatusListCred.getBitString.mapError(x => new Throwable(x.msg)) + _ <- ZIO.collectAll( + statusListWithCreds.credentials.map(c => + updateBitStringForCredentialAndNotify(bitString, c, walletAccessContext) + ) + ) + unprocessedEntityIds = statusListWithCreds.credentials.collect { + case x if !x.isProcessed && x.isCanceled => x.id + } + _ <- credentialStatusListService + .markAsProcessedMany(unprocessedEntityIds) + @@ Metric + .gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) - unprocessedEntityIds = statusListWithCreds.credentials.collect { - case x if !x.isProcessed && x.isCanceled => x.id - } - _ <- credentialStatusListService - .markAsProcessedMany(unprocessedEntityIds) - @@ Metric - .gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) + updatedVcStatusListCred <- vcStatusListCred.updateBitString(bitString).mapError { + case VCStatusList2021Error.EncodingError(msg: String) => new Throwable(msg) + case VCStatusList2021Error.DecodingError(msg: String) => new Throwable(msg) + } + vcStatusListCredJsonString <- updatedVcStatusListCred.toJsonWithEmbeddedProof.map(_.spaces2) + _ <- credentialStatusListService.updateStatusListCredential( + statusListWithCreds.id, + vcStatusListCredJsonString + ) + } yield () + _ <- effect + .catchAll(e => + ZIO.logErrorCause(s"Error processing status list record: ${statusListWithCreds.id} ", Cause.fail(e)) + ) + .catchAllDefect(d => + ZIO.logErrorCause(s"Defect processing status list record: ${statusListWithCreds.id}", Cause.fail(d)) + ) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } - updatedVcStatusListCred <- vcStatusListCred.updateBitString(bitString).mapError { - case VCStatusList2021Error.EncodingError(msg: String) => new Throwable(msg) - case VCStatusList2021Error.DecodingError(msg: String) => new Throwable(msg) - } - vcStatusListCredJsonString <- updatedVcStatusListCred.toJsonWithEmbeddedProof - .map(_.spaces2) - _ <- credentialStatusListService - .updateStatusListCredential(statusListWithCreds.id, vcStatusListCredJsonString) - } yield () + private def updateBitStringForCredentialAndNotify( + bitString: BitString, + credInStatusList: CredInStatusList, + walletAccessContext: WalletAccessContext + ) = { + for { + credentialService <- ZIO.service[CredentialService] + _ <- + if credInStatusList.isCanceled then { + val updateBitStringEffect = bitString.setRevokedInPlace(credInStatusList.statusListIndex, true) + val notifyEffect = sendRevocationNotificationMessage(credInStatusList) + val updateAndNotify = for { + updated <- updateBitStringEffect.mapError(x => new Throwable(x.message)) + _ <- + if !credInStatusList.isProcessed then + notifyEffect.flatMap { resp => + if (resp.status >= 200 && resp.status < 300) + ZIO.logInfo("successfully sent revocation notification message") + else ZIO.logError(s"failed to send revocation notification message") + } + else ZIO.unit + } yield updated + updateAndNotify.provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ Metric + .gauge("revocation_status_list_sync_process_single_credential_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } else ZIO.unit + } yield () + } - effect - .catchAll(e => - ZIO.logErrorCause(s"Error processing status list record: ${statusListWithCreds.id} ", Cause.fail(e)) - ) - .catchAllDefect(d => - ZIO.logErrorCause(s"Defect processing status list record: ${statusListWithCreds.id}", Cause.fail(d)) + private def sendRevocationNotificationMessage( + credInStatusList: CredInStatusList + ) = { + for { + credentialService <- ZIO.service[CredentialService] + maybeIssueCredentialRecord <- credentialService.findById(credInStatusList.issueCredentialRecordId) + issueCredentialRecord <- ZIO + .fromOption(maybeIssueCredentialRecord) + .mapError(_ => + new Throwable(s"Issue credential record not found by id: ${credInStatusList.issueCredentialRecordId}") + ) + issueCredentialData <- ZIO + .fromOption(issueCredentialRecord.issueCredentialData) + .mapError(_ => + new Throwable( + s"Issue credential data not found in issue credential record by id: ${credInStatusList.issueCredentialRecordId}" ) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - - } - config <- ZIO.service[AppConfig] - _ <- (ZIO - .collectAll(updatedVcStatusListsCredsEffects) @@ Metric - .gauge("revocation_status_list_sync_process_status_lists_w_creds_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .withParallelism(config.pollux.syncRevocationStatusesBgJobProcessingParallelism) - } yield () + ) + issueCredentialProtocolThreadId <- ZIO + .fromOption(issueCredentialData.thid) + .mapError(_ => new Throwable("thid not found in issue credential data")) + revocationNotification = RevocationNotification.build( + issueCredentialData.from, + issueCredentialData.to, + issueCredentialProtocolThreadId = issueCredentialProtocolThreadId + ) + didCommAgent <- buildDIDCommAgent(issueCredentialData.from) + response <- MessagingService + .send(revocationNotification.makeMessage) + .provideSomeLayer(didCommAgent) @@ Metric + .gauge("revocation_status_list_sync_revocation_notification_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } yield response + } } diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala index c7d1bd7a8f..4cb227e8ff 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala @@ -69,9 +69,9 @@ trait IssueControllerTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: IssueController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala index 6facffe5ed..fb4ce6f7ab 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala @@ -12,16 +12,15 @@ import org.hyperledger.identus.oid4vci.storage.InMemoryIssuanceSessionService import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration import org.hyperledger.identus.pollux.core.model.CredentialFormat import org.hyperledger.identus.pollux.core.repository.{ - CredentialRepository, CredentialRepositoryInMemory, CredentialStatusListRepositoryInMemory } import org.hyperledger.identus.pollux.core.service.* import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.PrismDidResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.{Clock, Random, URLayer, ZIO, ZLayer} -import zio.json.* import zio.json.ast.Json import zio.mock.MockSpecDefault import zio.test.* @@ -54,6 +53,8 @@ object OIDCCredentialIssuerServiceSpec GenericSecretStorageInMemory.layer, LinkSecretServiceImpl.layer, CredentialServiceImpl.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, OIDCCredentialIssuerServiceImpl.layer ) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala index 19636104a5..55b93f3007 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala @@ -97,9 +97,9 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]) = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend( diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala index 5917f13f1c..7f72581960 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala @@ -96,9 +96,9 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]) = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend( diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala index 95ed827fec..80f15ce237 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala @@ -41,9 +41,9 @@ trait SystemControllerTestTools { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: SystemController) = { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala index d3ca097e3d..e4da96b640 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala @@ -70,9 +70,9 @@ trait VcVerificationControllerTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: VcVerificationController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { diff --git a/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql b/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql new file mode 100644 index 0000000000..5a59c4a121 --- /dev/null +++ b/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql @@ -0,0 +1,19 @@ +-- Last used DID Index per wallet (solving race condition) +CREATE TABLE public.last_did_index_per_wallet +( + "wallet_id" UUID REFERENCES public.wallet ("wallet_id") NOT NULL PRIMARY KEY, + "last_used_index" INT NOT NULL +); + +ALTER TABLE public.last_did_index_per_wallet + ENABLE ROW LEVEL SECURITY; + +CREATE +POLICY last_did_index_per_wallet_wallet_isolation +ON public.last_did_index_per_wallet +USING (wallet_id = current_setting('app.current_wallet_id')::UUID); + +INSERT INTO public.last_did_index_per_wallet(wallet_id, last_used_index) +SELECT wallet_id, MAX(did_index) +FROM public.prism_did_wallet_state +GROUP BY wallet_id; \ No newline at end of file diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala index 861f3a8b10..37e8543b94 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala @@ -1,8 +1,7 @@ package org.hyperledger.identus.agent.walletapi.service import org.hyperledger.identus.agent.walletapi.model.* -import org.hyperledger.identus.agent.walletapi.model.error.* -import org.hyperledger.identus.agent.walletapi.model.error.given +import org.hyperledger.identus.agent.walletapi.model.error.{*, given} import org.hyperledger.identus.agent.walletapi.service.handler.{DIDCreateHandler, DIDUpdateHandler, PublicationHandler} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService.DEFAULT_MASTER_KEY_ID import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage, WalletSecretStorage} @@ -32,7 +31,6 @@ class ManagedDIDServiceImpl private[walletapi] ( override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, - createDIDSem: Semaphore ) extends ManagedDIDService { private val AGREEMENT_KEY_ID = KeyId("agreement") @@ -127,7 +125,7 @@ class ManagedDIDServiceImpl private[walletapi] ( def createAndStoreDID( didTemplate: ManagedDIDTemplate ): ZIO[WalletAccessContext, CreateManagedDIDError, LongFormPrismDID] = { - val effect = for { + for { _ <- ZIO .fromEither(ManagedDIDTemplateValidator.validate(didTemplate, defaultDidDocumentServices)) .mapError { x => @@ -144,15 +142,6 @@ class ManagedDIDServiceImpl private[walletapi] ( .mapError(CreateManagedDIDError.InvalidOperation.apply) _ <- material.persist.mapError(CreateManagedDIDError.WalletStorageError.apply) } yield PrismDID.buildLongFormFromOperation(material.operation) - - // This synchronizes createDID effect to only allow 1 execution at a time - // to avoid concurrent didIndex update. Long-term solution should be - // solved at the DB level. - // - // Performance may be improved by not synchronizing the whole operation, - // but only the counter increment part allowing multiple in-flight create operations - // once didIndex is acquired. - createDIDSem.withPermit(effect) } def updateManagedDID( @@ -385,7 +374,6 @@ object ManagedDIDServiceImpl { nonSecretStorage <- ZIO.service[DIDNonSecretStorage] walletSecretStorage <- ZIO.service[WalletSecretStorage] apollo <- ZIO.service[Apollo] - createDIDSem <- Semaphore.make(1) } yield ManagedDIDServiceImpl( defaultDidDocumentServices, didService, @@ -393,8 +381,7 @@ object ManagedDIDServiceImpl { secretStorage, nonSecretStorage, walletSecretStorage, - apollo, - createDIDSem + apollo ) } } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index 795e6d6199..02b5142152 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -20,7 +20,6 @@ class ManagedDIDServiceWithEventNotificationImpl( override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, - createDIDSem: Semaphore, eventNotificationService: EventNotificationService ) extends ManagedDIDServiceImpl( defaultDidDocumentServices, @@ -29,8 +28,7 @@ class ManagedDIDServiceWithEventNotificationImpl( secretStorage, nonSecretStorage, walletSecretStorage, - apollo, - createDIDSem + apollo ) { private val didStatusUpdatedEventName = "DIDStatusUpdated" @@ -81,7 +79,6 @@ object ManagedDIDServiceWithEventNotificationImpl { nonSecretStorage, walletSecretStorage, apollo, - createDIDSem, eventNotificationService ) } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala index 66fec256bb..d87ef1c91d 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala @@ -31,12 +31,7 @@ private[walletapi] class DIDCreateHandler( walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) seed <- walletSecretStorage.findWalletSeed .someOrElseZIO(ZIO.dieMessage(s"Wallet seed for wallet $walletId does not exist")) - didIndex <- nonSecretStorage - .getMaxDIDIndex() - .mapBoth( - CreateManagedDIDError.WalletStorageError.apply, - maybeIdx => maybeIdx.map(_ + 1).getOrElse(0) - ) + didIndex <- nonSecretStorage.incrementAndGetNextDIDIndex generated <- operationFactory.makeCreateOperation(masterKeyId, seed.toByteArray)(didIndex, didTemplate) (createOperation, keys) = generated state = ManagedDIDState(createOperation, didIndex, PublicationState.Created()) diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala index bfdca44f73..7f86258e80 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala @@ -1,27 +1,21 @@ package org.hyperledger.identus.agent.walletapi.sql +import cats.implicits.toFunctorOps import doobie.* import doobie.implicits.* import doobie.postgres.implicits.* import org.hyperledger.identus.agent.walletapi.model.* import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage -import org.hyperledger.identus.castor.core.model.did.{ - EllipticCurve, - InternalKeyPurpose, - PrismDID, - ScheduledDIDOperationStatus, - VerificationRelationship -} +import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.shared.db.ContextAwareTask -import org.hyperledger.identus.shared.db.Implicits.* -import org.hyperledger.identus.shared.db.Implicits.given +import org.hyperledger.identus.shared.db.Implicits.{*, given} import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext, WalletId} import zio.* import zio.interop.catz.* import java.time.Instant -import scala.collection.immutable.ArraySeq +import java.util.Objects class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) extends DIDNonSecretStorage { @@ -109,11 +103,11 @@ class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[T _ <- insertHdKeyIO.updateMany(randKeyValues(now)) } yield () - for { + (for { walletCtx <- ZIO.service[WalletAccessContext] now <- Clock.instant _ <- txnIO(now, walletCtx.walletId).transactWallet(xa) - } yield () + } yield ()).orDie } override def updateManagedDID(did: PrismDID, patch: ManagedDIDStatePatch): RIO[WalletAccessContext, Unit] = { @@ -151,6 +145,41 @@ class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[T cxnIO.transactWallet(xa).map(_.flatten) } + override def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, Int] = { + def acquireAdvisoryLock(walletId: WalletId): ConnectionIO[Unit] = { + // Should be specific to this process + val PROCESS_UNIQUE_ID = 465263 + val hashCode = Objects.hash(walletId.hashCode(), PROCESS_UNIQUE_ID) + sql"SELECT pg_advisory_xact_lock($hashCode)".query[Unit].unique.void + } + + def insertWalletDIDIndexIfNotExists(walletId: WalletId): ConnectionIO[Int] = { + sql""" + | INSERT INTO public.last_did_index_per_wallet (wallet_id, last_used_index) + | VALUES ($walletId, -1) + | ON CONFLICT (wallet_id) DO NOTHING""".stripMargin.update.run + } + + def incrementWalletDIDIndex(walletId: WalletId): ConnectionIO[Int] = { + sql""" + | UPDATE public.last_did_index_per_wallet + | SET last_used_index = last_used_index + 1 + | WHERE wallet_id = $walletId + | RETURNING last_used_index""".stripMargin.query[Int].unique + } + + for { + walletCtx <- ZIO.service[WalletAccessContext] + walletId = walletCtx.walletId + cnxIO = for { + _ <- acquireAdvisoryLock(walletId) + _ <- insertWalletDIDIndexIfNotExists(walletId) + index <- incrementWalletDIDIndex(walletId) + } yield index + index <- cnxIO.transactWallet(xa).orDie + } yield index + } + override def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] = { val status: ScheduledDIDOperationStatus = ScheduledDIDOperationStatus.Confirmed val cxnIO = diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala index 1830dc1600..612338b1ad 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala @@ -21,6 +21,8 @@ trait DIDNonSecretStorage { def getMaxDIDIndex(): RIO[WalletAccessContext, Option[Int]] + def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, Int] + def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] /** Return a tuple of key metadata and the operation hash */ diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala index f14df4d00d..6a29b2769d 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala @@ -49,6 +49,9 @@ case class MockDIDNonSecretStorage(proxy: Proxy) extends DIDNonSecretStorage { override def getMaxDIDIndex(): RIO[WalletAccessContext, Option[Int]] = proxy(MockDIDNonSecretStorage.GetMaxDIDIndex) + override def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, RuntimeFlags] = + proxy(MockDIDNonSecretStorage.IncrementAndGetNextDIDIndex) + override def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] = proxy(MockDIDNonSecretStorage.GetHdKeyCounter, did) @@ -89,6 +92,7 @@ object MockDIDNonSecretStorage extends Mock[DIDNonSecretStorage] { ] object UpdateManagedDID extends Effect[(PrismDID, ManagedDIDStatePatch), Throwable, Unit] object GetMaxDIDIndex extends Effect[Unit, Throwable, Option[Int]] + object IncrementAndGetNextDIDIndex extends Effect[Unit, Nothing, Int] object GetHdKeyCounter extends Effect[PrismDID, Throwable, Option[HdKeyIndexCounter]] object GetKeyMeta extends Effect[(PrismDID, KeyId), Throwable, Option[(ManagedDIDKeyMeta, Array[Byte])]] object InsertHdKeyMeta extends Effect[(PrismDID, KeyId, ManagedDIDKeyMeta, Array[Byte]), Throwable, Unit] diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala new file mode 100644 index 0000000000..687f5e9aa1 --- /dev/null +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala @@ -0,0 +1,19 @@ +//package org.hyperledger.identus.connect.core.model +// +//import org.hyperledger.identus.messaging.Serde +//import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} +// +//import java.nio.charset.StandardCharsets +//import java.util.UUID +// +//case class WalletIdAndRecordId(walletId: UUID, recordId: UUID) +// +//object WalletIdAndRecordId { +// given encoder: JsonEncoder[WalletIdAndRecordId] = DeriveJsonEncoder.gen[WalletIdAndRecordId] +// given decoder: JsonDecoder[WalletIdAndRecordId] = DeriveJsonDecoder.gen[WalletIdAndRecordId] +// given ser: Serde[WalletIdAndRecordId] = new Serde[WalletIdAndRecordId] { +// override def serialize(t: WalletIdAndRecordId): Array[Byte] = t.toJson.getBytes(StandardCharsets.UTF_8) +// override def deserialize(ba: Array[Byte]): WalletIdAndRecordId = +// new String(ba, StandardCharsets.UTF_8).fromJson[WalletIdAndRecordId].getOrElse(throw RuntimeException("")) +// } +//} diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala index a4072aea05..db91377abc 100644 --- a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala @@ -1,14 +1,13 @@ package org.hyperledger.identus.connect.core.service -import org.hyperledger.identus.* import org.hyperledger.identus.connect.core.model.{ConnectionRecord, ConnectionRecordBeforeStored} -import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.* import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.repository.ConnectionRepository import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.connection.* import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -21,9 +20,12 @@ import java.util.UUID private class ConnectionServiceImpl( connectionRepository: ConnectionRepository, + messageProducer: Producer[UUID, WalletIdAndRecordId], maxRetries: Int = 5, // TODO move to config ) extends ConnectionService { + private val TOPIC_NAME = "connect" + override def createConnectionInvitation( label: Option[String], goalCode: Option[String], @@ -147,6 +149,11 @@ private class ConnectionServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_invitee_pending_to_req_sent" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + // TODO Should we use a singleton producer or create a new one each time?? (underlying Kafka Producer is thread safe) + _ <- messageProducer + .produce(TOPIC_NAME, record.id, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id)) + .orDie maybeRecord <- connectionRepository .findById(record.id) record <- ZIO.getOrFailWith(RecordIdNotFound(recordId))(maybeRecord) @@ -220,6 +227,10 @@ private class ConnectionServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_inviter_pending_to_res_sent" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id)) + .orDie record <- connectionRepository.getById(record.id) } yield record @@ -306,6 +317,6 @@ private class ConnectionServiceImpl( } object ConnectionServiceImpl { - val layer: URLayer[ConnectionRepository, ConnectionService] = - ZLayer.fromFunction(ConnectionServiceImpl(_)) + val layer: URLayer[ConnectionRepository & Producer[UUID, WalletIdAndRecordId], ConnectionService] = + ZLayer.fromFunction(ConnectionServiceImpl(_, _)) } diff --git a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala index b0fa8d43fd..7067b55bf6 100644 --- a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala +++ b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala @@ -3,17 +3,17 @@ package org.hyperledger.identus.connect.core.service import io.circe.syntax.* import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.InvalidStateForOperation -import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.repository.ConnectionRepositoryInMemory import org.hyperledger.identus.mercury.model.{DidId, Message} import org.hyperledger.identus.mercury.protocol.connection.ConnectionResponse +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.test.* import zio.test.Assertion.* -import java.time.Instant import java.util.UUID object ConnectionServiceImplSpec extends ZIOSpecDefault { @@ -310,7 +310,13 @@ object ConnectionServiceImplSpec extends ZIOSpecDefault { } } } - ).provide(connectionServiceLayer, ZLayer.succeed(WalletAccessContext(WalletId.random))) + ).provide( + connectionServiceLayer, + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId], + ZLayer.succeed(WalletAccessContext(WalletId.random)), + ) } } diff --git a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala index b9e54811b9..185bd95b95 100644 --- a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala +++ b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala @@ -7,11 +7,12 @@ import org.hyperledger.identus.event.notification.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.mock.Expectation import zio.test.* -import zio.ZIO.* import java.time.Instant import java.util.UUID @@ -151,7 +152,10 @@ object ConnectionServiceNotifierSpec extends ZIOSpecDefault { ConnectionRepositoryInMemory.layer ++ inviteeExpectations.toLayer ) >>> ConnectionServiceNotifier.layer, - ZLayer.succeed(WalletAccessContext(WalletId.random)) + ZLayer.succeed(WalletAccessContext(WalletId.random)), + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId] ) ) } diff --git a/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala b/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala new file mode 100644 index 0000000000..54aea505f6 --- /dev/null +++ b/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala @@ -0,0 +1,49 @@ +package org.hyperledger.identus.messaging + +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, MessagingService, Serde} +import zio.{durationInt, Random, Schedule, Scope, URIO, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} +import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} + +import java.nio.charset.StandardCharsets +import java.util.UUID + +case class Customer(name: String) + +object Customer { + given encoder: JsonEncoder[Customer] = DeriveJsonEncoder.gen[Customer] + given decoder: JsonDecoder[Customer] = DeriveJsonDecoder.gen[Customer] + given serde: Serde[Customer] = new Serde[Customer]: + override def serialize(t: Customer): Array[Byte] = + t.toJson.getBytes(StandardCharsets.UTF_8) + override def deserialize(ba: Array[Byte]): Customer = + new String(ba, StandardCharsets.UTF_8).fromJson[Customer].getOrElse(Customer("Parsing Error")) +} + +object MessagingServiceTest extends ZIOAppDefault { + override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = { + val effect = for { + ms <- ZIO.service[MessagingService] + consumer <- ms.makeConsumer[UUID, Customer]("identus-cloud-agent") + producer <- ms.makeProducer[UUID, Customer]() + f1 <- consumer + .consume("Connect")(handle) + .fork + f2 <- Random.nextUUID + .flatMap(uuid => producer.produce("Connect", uuid, Customer(s"Name $uuid"))) + .repeat(Schedule.spaced(500.millis)) + .fork + _ <- ZIO.never + } yield () + effect.provide( + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + ZLayer.succeed("Sample 'R' passed to handler") + ) + } + + def handle[K, V](msg: Message[K, V]): URIO[String, Unit] = for { + tag <- ZIO.service[String] + _ <- ZIO.logInfo(s"Handling new message [$tag]: ${msg.offset} - ${msg.key} - ${msg.value}") + } yield () +} diff --git a/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala b/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala new file mode 100644 index 0000000000..c6b068f16b --- /dev/null +++ b/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala @@ -0,0 +1,66 @@ +package org.hyperledger.identus.messaging.kafka + +import org.hyperledger.identus.shared.messaging.* +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object InMemoryMessagingServiceSpec extends ZIOSpecDefault { + val testLayer = MessagingServiceConfig.inMemoryLayer >+> MessagingService.serviceLayer >+> + MessagingService.producerLayer[String, String] >+> + MessagingService.consumerLayer[String, String]("test-group") + + def spec = suite("InMemoryMessagingServiceSpec")( + test("should produce and consume messages") { + + val key = "key" + val value = "value" + val topic = "test-topic" + for { + producer <- ZIO.service[Producer[String, String]] + consumer <- ZIO.service[Consumer[String, String]] + promise <- Promise.make[Nothing, Message[String, String]] + _ <- producer.produce(topic, key, value) + _ <- consumer + .consume(topic) { msg => + promise.succeed(msg).unit + } + .fork + receivedMessage <- promise.await + } yield assert(receivedMessage)(equalTo(Message(key, value, 1L, 0))) + }.provideLayer(testLayer), + test("should produce and consume 5 messages") { + val topic = "test-topic" + val messages = List( + ("key1", "value1"), + ("key2", "value2"), + ("key3", "value3"), + ("key4", "value4"), + ("key5", "value5") + ) + + for { + producer <- ZIO.service[Producer[String, String]] + consumer <- ZIO.service[Consumer[String, String]] + promise <- Promise.make[Nothing, List[Message[String, String]]] + ref <- Ref.make(List.empty[Message[String, String]]) + + _ <- ZIO.foreach(messages) { case (key, value) => + producer.produce(topic, key, value) *> ZIO.debug(s"Produced message: $key -> $value") + } + _ <- consumer + .consume(topic) { msg => + ZIO.debug(s"Consumed message: ${msg.key} -> ${msg.value}") *> + ref.update(_ :+ msg) *> ref.get.flatMap { msgs => + if (msgs.size == messages.size) promise.succeed(msgs).unit else ZIO.unit + } + } + .fork + receivedMessages <- promise.await + _ <- ZIO.debug(s"Received messages: ${receivedMessages.map(m => (m.key, m.value))}") + } yield assert(receivedMessages.map(m => (m.key, m.value)).sorted)( + equalTo(messages.sorted) + ) + }.provideLayer(testLayer), + ) +} diff --git a/infrastructure/shared/docker-compose-with-kafka.yml b/infrastructure/shared/docker-compose-with-kafka.yml new file mode 100644 index 0000000000..3e24ad6128 --- /dev/null +++ b/infrastructure/shared/docker-compose-with-kafka.yml @@ -0,0 +1,256 @@ +--- +services: + ########################## + # Database + ########################## + db: + image: postgres:13 + environment: + POSTGRES_MULTIPLE_DATABASES: "pollux,connect,agent,node_db" + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - pg_data_db:/var/lib/postgresql/data + - ./postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ./postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + ports: + - "127.0.0.1:${PG_PORT:-5432}:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "agent"] + + interval: 10s + timeout: 5s + retries: 5 + + pgadmin: + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: "False" + volumes: + - pgadmin:/var/lib/pgadmin + ports: + - "127.0.0.1:${PGADMIN_PORT:-5050}:80" + depends_on: + db: + condition: service_healthy + profiles: + - debug + + ########################## + # Services + ########################## + + prism-node: + image: ghcr.io/input-output-hk/prism-node:${PRISM_NODE_VERSION} + environment: + NODE_PSQL_HOST: db:5432 + NODE_REFRESH_AND_SUBMIT_PERIOD: + NODE_MOVE_SCHEDULED_TO_PENDING_PERIOD: + NODE_WALLET_MAX_TPS: + depends_on: + db: + condition: service_healthy + + vault-server: + image: hashicorp/vault:latest + # ports: + # - "8200:8200" + environment: + VAULT_ADDR: "http://0.0.0.0:8200" + VAULT_DEV_ROOT_TOKEN_ID: ${VAULT_DEV_ROOT_TOKEN_ID} + command: server -dev -dev-root-token-id=${VAULT_DEV_ROOT_TOKEN_ID} + cap_add: + - IPC_LOCK + healthcheck: + test: ["CMD", "vault", "status"] + interval: 10s + timeout: 5s + retries: 5 + + cloud-agent: + image: ghcr.io/hyperledger/identus-cloud-agent:${AGENT_VERSION} + environment: + POLLUX_DB_HOST: db + POLLUX_DB_PORT: 5432 + POLLUX_DB_NAME: pollux + POLLUX_DB_USER: postgres + POLLUX_DB_PASSWORD: postgres + CONNECT_DB_HOST: db + CONNECT_DB_PORT: 5432 + CONNECT_DB_NAME: connect + CONNECT_DB_USER: postgres + CONNECT_DB_PASSWORD: postgres + AGENT_DB_HOST: db + AGENT_DB_PORT: 5432 + AGENT_DB_NAME: agent + AGENT_DB_USER: postgres + AGENT_DB_PASSWORD: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://${DOCKERHOST}:${PORT}/cloud-agent + DIDCOMM_SERVICE_URL: http://${DOCKERHOST}:${PORT}/didcomm + REST_SERVICE_URL: http://${DOCKERHOST}:${PORT}/cloud-agent + PRISM_NODE_HOST: prism-node + PRISM_NODE_PORT: 50053 + VAULT_ADDR: ${VAULT_ADDR:-http://vault-server:8200} + VAULT_TOKEN: ${VAULT_DEV_ROOT_TOKEN_ID:-root} + SECRET_STORAGE_BACKEND: postgres + DEV_MODE: true + DEFAULT_WALLET_ENABLED: + DEFAULT_WALLET_SEED: + DEFAULT_WALLET_WEBHOOK_URL: + DEFAULT_WALLET_WEBHOOK_API_KEY: + DEFAULT_WALLET_AUTH_API_KEY: + DEFAULT_KAFKA_ENABLED: true + GLOBAL_WEBHOOK_URL: + GLOBAL_WEBHOOK_API_KEY: + WEBHOOK_PARALLELISM: + ADMIN_TOKEN: + API_KEY_SALT: + API_KEY_ENABLED: + API_KEY_AUTHENTICATE_AS_DEFAULT_USER: + API_KEY_AUTO_PROVISIONING: + depends_on: + db: + condition: service_healthy + prism-node: + condition: service_started + vault-server: + condition: service_healthy + init-kafka: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://cloud-agent:8085/_system/health"] + interval: 30s + timeout: 10s + retries: 5 + extra_hosts: + - "host.docker.internal:host-gateway" + + swagger-ui: + image: swaggerapi/swagger-ui:v5.1.0 + environment: + - 'URLS=[ + { name: "Cloud Agent", url: "/docs/cloud-agent/api/docs.yaml" } + ]' + + # apisix: + # image: apache/apisix:2.15.0-alpine + # volumes: + # - ./apisix/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro + # - ./apisix/conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro + # ports: + # - "${PORT}:9080/tcp" + # depends_on: + # - cloud-agent + # - swagger-ui + + nginx: + image: nginx:latest + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "${PORT}:80/tcp" + depends_on: + - cloud-agent + - swagger-ui + + zookeeper: + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + # ports: + # - 22181:2181 + + kafka: + image: confluentinc/cp-kafka:latest + depends_on: + - zookeeper + # ports: + # - 29092:29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: false + healthcheck: + test: + [ + "CMD", + "kafka-topics", + "--list", + "--bootstrap-server", + "localhost:9092", + ] + interval: 5s + timeout: 10s + retries: 5 + + init-kafka: + image: confluentinc/cp-kafka:latest + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/sh", "-c"] + command: | + " + # blocks until kafka is reachable + kafka-topics --bootstrap-server kafka:9092 --list + echo -e 'Creating kafka topics' + + # Connect + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-1 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-2 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-3 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-4 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-DLQ --replication-factor 1 --partitions 1 + + # Issue + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-DLQ --replication-factor 1 --partitions 1 + + # Present + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-DLQ --replication-factor 1 --partitions 1 + + # DID Publication State Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state-DLQ --replication-factor 1 --partitions 5 + + # Status List Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list-DLQ --replication-factor 1 --partitions 5 + + tail -f /dev/null + " + healthcheck: + test: + [ + "CMD-SHELL", + "kafka-topics --bootstrap-server kafka:9092 --list | grep -q 'sync-status-list'", + ] + interval: 5s + timeout: 10s + retries: 5 + +volumes: + pg_data_db: + pgadmin: +# Temporary commit network setting due to e2e CI bug +# to be enabled later after debugging +#networks: +# default: +# name: ${NETWORK} diff --git a/infrastructure/shared/nginx/nginx.conf b/infrastructure/shared/nginx/nginx.conf new file mode 100644 index 0000000000..937dc35a3f --- /dev/null +++ b/infrastructure/shared/nginx/nginx.conf @@ -0,0 +1,42 @@ +user nginx; + +events { + worker_connections 1000; +} + +http { + # Docker embedded DNS server (overriding TTL) + resolver 127.0.0.11 valid=5s; + + # Upstreams + upstream cloud_agent_8090 { + server cloud-agent:8090; + } + + upstream cloud_agent_8085 { + server cloud-agent:8085; + } + + # Server configuration + server { + listen 80; + + # Route /cloud-agent/* + location ~ ^/cloud-agent/(.*) { + # Proxy rewrite + set $upstream_servers cloud-agent; + rewrite ^/cloud-agent/(.*) /$1 break; + proxy_pass http://$upstream_servers:8085; + proxy_connect_timeout 5s; + } + + # Route /didcomm* + location ~ ^/didcomm(.*) { + # Proxy rewrite + set $upstream_servers cloud-agent; + rewrite ^/didcomm(.*) /$1 break; + proxy_pass http://$upstream_servers:8090; + proxy_connect_timeout 5s; + } + } +} \ No newline at end of file diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala index 47fcced892..ed18017d45 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala @@ -7,4 +7,6 @@ opaque type DidCommID = String object DidCommID: def apply(value: String): DidCommID = value def apply(): DidCommID = UUID.randomUUID.toString() - extension (id: DidCommID) def value: String = id + extension (id: DidCommID) + def value: String = id + def uuid: UUID = UUID.fromString(id) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala index 6a86509592..6d8cdbb50d 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala @@ -1,30 +1,61 @@ package org.hyperledger.identus.pollux.core.repository import org.hyperledger.identus.pollux.core.model.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021} +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{ + DecodingError, + EncodingError, + IndexOutOfBounds, + InvalidSize +} import org.hyperledger.identus.pollux.vc.jwt.Issuer -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID trait CredentialStatusListRepository { - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] + def createStatusListVC( + jwtIssuer: Issuer, + statusListRegistryUrl: String, + id: UUID + ): IO[Throwable, String] = { + for { + bitString <- BitString.getInstance().mapError { + case InvalidSize(message) => new Throwable(message) + case EncodingError(message) => new Throwable(message) + case DecodingError(message) => new Throwable(message) + case IndexOutOfBounds(message) => new Throwable(message) + } + emptyStatusListCredential <- VCStatusList2021 + .build( + vcId = s"$statusListRegistryUrl/credential-status/$id", + revocationData = bitString, + jwtIssuer = jwtIssuer + ) + .mapError(x => new Throwable(x.msg)) + + credentialWithEmbeddedProof <- emptyStatusListCredential.toJsonWithEmbeddedProof + } yield credentialWithEmbeddedProof.spaces2 + } + + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] + + def getCredentialStatusListsWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] def findById( id: UUID ): UIO[Option[CredentialStatusList]] - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] + def incrementAndGetStatusListIndex( + jwtIssuer: Issuer, + statusListRegistryUrl: String + ): URIO[WalletAccessContext, (UUID, Int)] def existsForIssueCredentialRecordId( id: DidCommID ): URIO[WalletAccessContext, Boolean] - def createNewForTheWallet( - jwtIssuer: Issuer, - statusListRegistryServiceName: String - ): URIO[WalletAccessContext, CredentialStatusList] - def allocateSpaceForCredential( issueCredentialRecordId: DidCommID, credentialStatusListId: UUID, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index 4e059f8e05..a95e80925e 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -28,6 +28,7 @@ import org.hyperledger.identus.pollux.sdjwt.* import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *} import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair} import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -42,7 +43,8 @@ import scala.language.implicitConversions object CredentialServiceImpl { val layer: URLayer[ CredentialRepository & CredentialStatusListRepository & DidResolver & UriResolver & GenericSecretStorage & - CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService, + CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService & + Producer[UUID, WalletIdAndRecordId], CredentialService ] = { ZLayer.fromZIO { @@ -56,7 +58,7 @@ object CredentialServiceImpl { linkSecretService <- ZIO.service[LinkSecretService] didService <- ZIO.service[DIDService] manageDidService <- ZIO.service[ManagedDIDService] - issueCredentialSem <- Semaphore.make(1) + messageProducer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]] } yield CredentialServiceImpl( credentialRepo, credentialStatusListRepo, @@ -68,7 +70,7 @@ object CredentialServiceImpl { didService, manageDidService, 5, - issueCredentialSem + messageProducer ) } } @@ -88,12 +90,14 @@ class CredentialServiceImpl( didService: DIDService, managedDIDService: ManagedDIDService, maxRetries: Int = 5, // TODO move to config - issueCredentialSem: Semaphore + messageProducer: Producer[UUID, WalletIdAndRecordId], ) extends CredentialService { import CredentialServiceImpl.* import IssueCredentialRecord.* + private val TOPIC_NAME = "issue" + override def getIssueCredentialRecords( ignoreWithZeroRetries: Boolean, offset: Option[Int], @@ -187,6 +191,10 @@ class CredentialServiceImpl( count <- credentialRepository .create(record) @@ CustomMetricsAspect .startRecordingTime(s"${record.id}_issuer_offer_pending_to_sent_ms_gauge") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -500,6 +508,10 @@ class CredentialServiceImpl( ) case (format, maybeSubjectId) => ZIO.dieMessage(s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -661,6 +673,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_holder_req_pending_to_generated", "issuance_flow_holder_req_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -707,6 +723,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_holder_req_pending_to_generated", "issuance_flow_holder_req_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -751,6 +771,10 @@ class CredentialServiceImpl( ProtocolState.OfferSent ) _ <- credentialRepository.updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -769,6 +793,10 @@ class CredentialServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_issuance_flow_issuer_credential_pending_to_generated" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -913,6 +941,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_issuer_credential_pending_to_generated", "issuance_flow_issuer_credential_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -1275,32 +1307,26 @@ class CredentialServiceImpl( record: IssueCredentialRecord, statusListRegistryUrl: String, jwtIssuer: JwtIssuer - ): URIO[WalletAccessContext, CredentialStatus] = { - val effect = for { - lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet - currentStatusList <- lastStatusList - .fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))( - ZIO.succeed(_) - ) - size = currentStatusList.size - lastUsedIndex = currentStatusList.lastUsedIndex - statusListToBeUsed <- - if lastUsedIndex < size then ZIO.succeed(currentStatusList) - else credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl) + ): URIO[WalletAccessContext, CredentialStatus] = + for { + cslAndIndex <- credentialStatusListRepository.incrementAndGetStatusListIndex( + jwtIssuer, + statusListRegistryUrl + ) + statusListId = cslAndIndex._1 + indexInStatusList = cslAndIndex._2 _ <- credentialStatusListRepository.allocateSpaceForCredential( issueCredentialRecordId = record.id, - credentialStatusListId = statusListToBeUsed.id, - statusListIndex = statusListToBeUsed.lastUsedIndex + 1 + credentialStatusListId = statusListId, + statusListIndex = indexInStatusList ) } yield CredentialStatus( - id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}", + id = s"$statusListRegistryUrl/credential-status/$statusListId#$indexInStatusList", `type` = "StatusList2021Entry", statusPurpose = StatusPurpose.Revocation, - statusListIndex = lastUsedIndex + 1, - statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}" + statusListIndex = indexInStatusList, + statusListCredential = s"$statusListRegistryUrl/credential-status/$statusListId" ) - issueCredentialSem.withPermit(effect) - } override def generateAnonCredsCredential( recordId: DidCommID diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala index 5a186d2826..418b3faa0c 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala @@ -6,7 +6,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServi StatusListNotFound, StatusListNotFoundForIssueCredentialRecord } -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID @@ -20,7 +20,9 @@ trait CredentialStatusListService { id: DidCommID ): ZIO[WalletAccessContext, StatusListNotFoundForIssueCredentialRecord | InvalidRoleForOperation, Unit] - def getCredentialsAndItsStatuses: UIO[Seq[CredentialStatusListWithCreds]] + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] + + def getCredentialStatusListWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] def updateStatusListCredential( id: UUID, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala index 92565a8559..ef752f8648 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala @@ -8,7 +8,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServi } import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role import org.hyperledger.identus.pollux.core.repository.CredentialStatusListRepository -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID @@ -18,8 +18,11 @@ class CredentialStatusListServiceImpl( credentialStatusListRepository: CredentialStatusListRepository, ) extends CredentialStatusListService { - def getCredentialsAndItsStatuses: UIO[Seq[CredentialStatusListWithCreds]] = - credentialStatusListRepository.getCredentialStatusListsWithCreds + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = + credentialStatusListRepository.getCredentialStatusListIds + + def getCredentialStatusListWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] = + credentialStatusListRepository.getCredentialStatusListsWithCreds(statusListId) def getById(id: UUID): IO[StatusListNotFound, CredentialStatusList] = for { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala index 94aa1af79b..20426ec477 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala @@ -105,7 +105,7 @@ trait PresentationService { def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] + ): URIO[WalletAccessContext, Option[PresentationRecord]] def findPresentationRecordByThreadId( thid: DidCommID diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala index 8200595ebf..3d18a25bbe 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala @@ -20,6 +20,7 @@ import org.hyperledger.identus.pollux.core.service.serdes.* import org.hyperledger.identus.pollux.sdjwt.{CredentialCompact, HolderPrivateKey, PresentationCompact, SDJWT} import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -37,11 +38,14 @@ private class PresentationServiceImpl( linkSecretService: LinkSecretService, presentationRepository: PresentationRepository, credentialRepository: CredentialRepository, - maxRetries: Int = 5, // TODO move to config + messageProducer: Producer[UUID, WalletIdAndRecordId], + maxRetries: Int = 5, // TODO move to config, ) extends PresentationService { import PresentationRecord.* + private val TOPIC_NAME = "present" + override def markPresentationGenerated( recordId: DidCommID, presentation: Presentation @@ -57,6 +61,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_generated_to_sent_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -298,7 +306,7 @@ private class PresentationServiceImpl( override def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] = + ): URIO[WalletAccessContext, Option[PresentationRecord]] = presentationRepository.findPresentationRecord(recordId) override def findPresentationRecordByThreadId( @@ -459,6 +467,10 @@ private class PresentationServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_verifier_req_pending_to_sent_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -531,6 +543,10 @@ private class PresentationServiceImpl( ) _ <- presentationRepository.createPresentationRecord(record) _ <- ZIO.logDebug(s"Received and created the RequestPresentation: $request") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -813,6 +829,10 @@ private class PresentationServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -841,6 +861,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -875,6 +899,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -961,6 +989,10 @@ private class PresentationServiceImpl( .startRecordingTime( s"${record.id}_present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -977,6 +1009,10 @@ private class PresentationServiceImpl( requestPresentation = createDidCommRequestPresentationFromProposal(request) _ <- presentationRepository .updateWithRequestPresentation(recordId, requestPresentation, ProtocolState.PresentationPending) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -993,6 +1029,10 @@ private class PresentationServiceImpl( record <- getRecordFromThreadId(thid) _ <- presentationRepository .updateWithProposePresentation(record.id, proposePresentation, ProtocolState.ProposalReceived) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -1305,8 +1345,9 @@ private class PresentationServiceImpl( object PresentationServiceImpl { val layer: URLayer[ - UriResolver & LinkSecretService & PresentationRepository & CredentialRepository, + UriResolver & LinkSecretService & PresentationRepository & CredentialRepository & + Producer[UUID, WalletIdAndRecordId], PresentationService ] = - ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _)) + ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _, _)) } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala index 80d358cdbf..350c161100 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala @@ -14,6 +14,7 @@ import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCr import org.hyperledger.identus.shared.models.* import zio.* import zio.json.* +import zio.URIO import java.time.Instant import java.util.UUID @@ -275,7 +276,7 @@ class PresentationServiceNotifier( override def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] = + ): URIO[WalletAccessContext, Option[PresentationRecord]] = svc.findPresentationRecord(recordId) override def findPresentationRecordByThreadId( diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala index 8ea5ca21d1..3e4f885f2b 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala @@ -1,15 +1,9 @@ package org.hyperledger.identus.pollux.core.repository -import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID} +import org.hyperledger.identus.castor.core.model.did.PrismDID import org.hyperledger.identus.pollux.core.model.* -import org.hyperledger.identus.pollux.vc.jwt.{revocation, Issuer, StatusPurpose} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021} -import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{ - DecodingError, - EncodingError, - IndexOutOfBounds, - InvalidSize -} +import org.hyperledger.identus.pollux.vc.jwt.{Issuer, StatusPurpose} +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -73,65 +67,69 @@ class CredentialStatusListRepositoryInMemory( exists = stores.flatMap(_.values).exists(_.issueCredentialRecordId == id) } yield exists - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for { - storageRef <- walletToStatusListStorageRefs - storage <- storageRef.get - latest = storage.toSeq - .sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ } - .headOption - .map(_._2) - } yield latest - - def createNewForTheWallet( + override def incrementAndGetStatusListIndex( jwtIssuer: Issuer, statusListRegistryUrl: String - ): URIO[WalletAccessContext, CredentialStatusList] = { + ): URIO[WalletAccessContext, (UUID, Int)] = + def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for { + storageRef <- walletToStatusListStorageRefs + storage <- storageRef.get + latest = storage.toSeq + .sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ } + .headOption + .map(_._2) + } yield latest - val id = UUID.randomUUID() - val issued = Instant.now() - val issuerDid = jwtIssuer.did - val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical) + def createNewForTheWallet( + id: UUID, + jwtIssuer: Issuer, + issued: Instant, + credentialStr: String + ): URIO[WalletAccessContext, CredentialStatusList] = { + val issuerDid = jwtIssuer.did + val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical) - val embeddedProofCredential = for { - bitString <- BitString.getInstance().mapError { - case InvalidSize(message) => new Throwable(message) - case EncodingError(message) => new Throwable(message) - case DecodingError(message) => new Throwable(message) - case IndexOutOfBounds(message) => new Throwable(message) - } - resourcePath = - s"credential-status/$id" - emptyJwtCredential <- VCStatusList2021 - .build( - vcId = s"$statusListRegistryUrl/credential-status/$id", - revocationData = bitString, - jwtIssuer = jwtIssuer + for { + storageRef <- walletToStatusListStorageRefs + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + newCredentialStatusList = CredentialStatusList( + id = id, + walletId = walletId, + issuer = canonical, + issued = issued, + purpose = StatusPurpose.Revocation, + statusListCredential = credentialStr, + size = BitString.MIN_SL2021_SIZE, + lastUsedIndex = 0, + createdAt = Instant.now(), + updatedAt = None ) - .mapError(x => new Throwable(x.msg)) + _ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList)) + } yield newCredentialStatusList + } - credentialWithEmbeddedProof <- emptyJwtCredential.toJsonWithEmbeddedProof - } yield credentialWithEmbeddedProof.spaces2 + def updateLastUsedIndex(statusListId: UUID, lastUsedIndex: Int) = + for { + walletToStatusListStorageRef <- walletToStatusListStorageRefs + _ <- walletToStatusListStorageRef.update(r => { + val value = r.get(statusListId) + value.fold(r) { v => + val updated = v.copy(lastUsedIndex = lastUsedIndex, updatedAt = Some(Instant.now)) + r.updated(statusListId, updated) + } + }) + } yield () for { - credential <- embeddedProofCredential.orDie - storageRef <- walletToStatusListStorageRefs - walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) - newCredentialStatusList = CredentialStatusList( - id = id, - walletId = walletId, - issuer = canonical, - issued = issued, - purpose = StatusPurpose.Revocation, - statusListCredential = credential, - size = BitString.MIN_SL2021_SIZE, - lastUsedIndex = 0, - createdAt = Instant.now(), - updatedAt = None - ) - _ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList)) - } yield newCredentialStatusList - - } + id <- ZIO.succeed(UUID.randomUUID()) + newStatusListVC <- createStatusListVC(jwtIssuer, statusListRegistryUrl, id).orDie + maybeStatusList <- getLatestOfTheWallet + statusList <- maybeStatusList match + case Some(csl) if csl.lastUsedIndex < csl.size => ZIO.succeed(csl) + case _ => createNewForTheWallet(id, jwtIssuer, Instant.now(), newStatusListVC) + newIndex = statusList.lastUsedIndex + 1 + _ <- updateLastUsedIndex(statusList.id, newIndex) + } yield (statusList.id, newIndex) def allocateSpaceForCredential( issueCredentialRecordId: DidCommID, @@ -152,14 +150,6 @@ class CredentialStatusListRepositoryInMemory( for { credentialInStatusListStorageRef <- statusListToCredInStatusListStorageRefs(credentialStatusListId) _ <- credentialInStatusListStorageRef.update(r => r + (newCredentialInStatusList.id -> newCredentialInStatusList)) - walletToStatusListStorageRef <- walletToStatusListStorageRefs - _ <- walletToStatusListStorageRef.update(r => { - val value = r.get(credentialStatusListId) - value.fold(r) { v => - val updated = v.copy(lastUsedIndex = statusListIndex, updatedAt = Some(Instant.now)) - r.updated(credentialStatusListId, updated) - } - }) } yield () } @@ -188,37 +178,39 @@ class CredentialStatusListRepositoryInMemory( } yield () } - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] = { + override def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = for { statusListsRefs <- allStatusListsStorageRefs statusLists <- statusListsRefs.get - statusListWithCredEffects = statusLists.map { (id, statusList) => - val credsinStatusListEffect = statusListToCredInStatusListStorageRefs(id).flatMap(_.get.map(_.values.toList)) - credsinStatusListEffect.map { credsInStatusList => - CredentialStatusListWithCreds( - id = id, - walletId = statusList.walletId, - issuer = statusList.issuer, - issued = statusList.issued, - purpose = statusList.purpose, - statusListCredential = statusList.statusListCredential, - size = statusList.size, - lastUsedIndex = statusList.lastUsedIndex, - credentials = credsInStatusList.map { cred => - CredInStatusList( - id = cred.id, - issueCredentialRecordId = cred.issueCredentialRecordId, - statusListIndex = cred.statusListIndex, - isCanceled = cred.isCanceled, - isProcessed = cred.isProcessed, - ) - } - ) - } + } yield statusLists.values.toList.map(csl => (csl.walletId, csl.id)) - }.toList - res <- ZIO.collectAll(statusListWithCredEffects) - } yield res + def getCredentialStatusListsWithCreds( + statusListId: UUID + ): URIO[WalletAccessContext, CredentialStatusListWithCreds] = { + for { + statusListsRefs <- allStatusListsStorageRefs + statusLists <- statusListsRefs.get + statusList = statusLists(statusListId) + credsInStatusList <- statusListToCredInStatusListStorageRefs(statusList.id).flatMap(_.get.map(_.values.toList)) + } yield CredentialStatusListWithCreds( + id = statusList.id, + walletId = statusList.walletId, + issuer = statusList.issuer, + issued = statusList.issued, + purpose = statusList.purpose, + statusListCredential = statusList.statusListCredential, + size = statusList.size, + lastUsedIndex = statusList.lastUsedIndex, + credentials = credsInStatusList.map { cred => + CredInStatusList( + id = cred.id, + issueCredentialRecordId = cred.issueCredentialRecordId, + statusListIndex = cred.statusListIndex, + isCanceled = cred.isCanceled, + isProcessed = cred.isProcessed, + ) + } + ) } def updateStatusListCredential( diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index a03eb88803..8ae2fd6602 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -3,7 +3,6 @@ package org.hyperledger.identus.pollux.core.service import io.circe.Json import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.castor.core.model.did.PrismDID import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.mercury.model.{AttachmentDescriptor, DidId} @@ -18,6 +17,7 @@ import org.hyperledger.identus.pollux.core.repository.{ import org.hyperledger.identus.pollux.prex.{ClaimFormat, Ldp, PresentationDefinition} import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -41,6 +41,8 @@ trait CredentialServiceSpecHelper { credentialDefinitionServiceLayer, GenericSecretStorageInMemory.layer, LinkSecretServiceImpl.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, CredentialServiceImpl.layer ) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala index 4f58c2f570..dad75bfcee 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala @@ -16,7 +16,7 @@ import org.hyperledger.identus.pollux.core.service.serdes.{AnoncredCredentialPro import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, PresentationCompact} import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} import org.hyperledger.identus.shared.models.* -import zio.{mock, Duration, IO, UIO, URLayer, ZIO, ZLayer} +import zio.{mock, Duration, IO, UIO, URIO, URLayer, ZIO, ZLayer} import zio.json.* import zio.mock.{Mock, Proxy} @@ -329,7 +329,8 @@ object MockPresentationService extends Mock[PresentationService] { state: PresentationRecord.ProtocolState* ): IO[PresentationError, Seq[PresentationRecord]] = ??? - override def findPresentationRecord(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = ??? + override def findPresentationRecord(recordId: DidCommID): URIO[WalletAccessContext, Option[PresentationRecord]] = + ??? override def findPresentationRecordByThreadId( thid: DidCommID diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala index e438d4687b..1e59c4704a 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala @@ -1,6 +1,5 @@ package org.hyperledger.identus.pollux.core.service -import com.nimbusds.jose.jwk.* import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.castor.core.model.did.DID import org.hyperledger.identus.mercury.{AgentPeerService, PeerDID} @@ -14,6 +13,7 @@ import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResol import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.crypto.KmpSecp256k1KeyOps import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -42,7 +42,9 @@ trait PresentationServiceSpecHelper { uriResolverLayer, linkSecretLayer, PresentationRepositoryInMemory.layer, - CredentialRepositoryInMemory.layer + CredentialRepositoryInMemory.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, ) ++ defaultWalletLayer def createIssuer(did: String): Issuer = { diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala index f0a9dc2dbe..f4b27410cf 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.pollux.sql.repository +import cats.implicits.toFunctorOps import doobie.* +import doobie.free.connection.ConnectionOp import doobie.implicits.* import doobie.postgres.* import doobie.postgres.implicits.* @@ -8,8 +10,7 @@ import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.repository.CredentialStatusListRepository import org.hyperledger.identus.pollux.vc.jwt.{Issuer, StatusPurpose} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, BitStringError, VCStatusList2021} -import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* import org.hyperledger.identus.shared.db.Implicits.given @@ -18,7 +19,7 @@ import zio.* import zio.interop.catz.* import java.time.Instant -import java.util.UUID +import java.util.{Objects, UUID} class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) extends CredentialStatusListRepository { @@ -47,9 +48,19 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T .orDie } - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = { + override def incrementAndGetStatusListIndex( + jwtIssuer: Issuer, + statusListRegistryUrl: String + ): URIO[WalletAccessContext, (UUID, Int)] = { - val cxnIO = + def acquireAdvisoryLock(walletId: WalletId): ConnectionIO[Unit] = { + // Should be specific to this process + val PROCESS_UNIQUE_ID = 235457 + val hashCode = Objects.hash(walletId.hashCode(), PROCESS_UNIQUE_ID) + sql"SELECT pg_advisory_xact_lock($hashCode)".query[Unit].unique.void + } + + def getLatestOfTheWallet: ConnectionIO[Option[CredentialStatusList]] = sql""" | SELECT | id, @@ -62,74 +73,70 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T | last_used_index, | created_at, | updated_at - | FROM public.credential_status_lists order by created_at DESC limit 1 + | FROM public.credential_status_lists + | ORDER BY created_at DESC limit 1 |""".stripMargin .query[CredentialStatusList] .option - cxnIO - .transactWallet(xa) - .orDie - - } + def createNewForTheWallet( + id: UUID, + issuerDid: String, + issued: Instant, + credentialStr: String + ): ConnectionIO[CredentialStatusList] = + sql""" + |INSERT INTO public.credential_status_lists ( + | id, + | issuer, + | issued, + | purpose, + | status_list_credential, + | size, + | last_used_index, + | wallet_id + | ) + |VALUES ( + | $id, + | $issuerDid, + | $issued, + | ${StatusPurpose.Revocation}::public.enum_credential_status_list_purpose, + | $credentialStr::JSON, + | ${BitString.MIN_SL2021_SIZE}, + | 0, + | current_setting('app.current_wallet_id')::UUID + | ) + |RETURNING id, wallet_id, issuer, issued, purpose, status_list_credential, size, last_used_index, created_at, updated_at + """.stripMargin + .query[CredentialStatusList] + .unique - def createNewForTheWallet( - jwtIssuer: Issuer, - statusListRegistryUrl: String - ): URIO[WalletAccessContext, CredentialStatusList] = { - - val id = UUID.randomUUID() - val issued = Instant.now() - val issuerDid = jwtIssuer.did.toString - - val credentialWithEmbeddedProof = for { - bitString <- BitString.getInstance().mapError { - case InvalidSize(message) => new Throwable(message) - case EncodingError(message) => new Throwable(message) - case DecodingError(message) => new Throwable(message) - case IndexOutOfBounds(message) => new Throwable(message) - } - resourcePath = - s"credential-status/$id" - emptyStatusListCredential <- VCStatusList2021 - .build( - vcId = s"$statusListRegistryUrl/credential-status/$id", - revocationData = bitString, - jwtIssuer = jwtIssuer - ) - .mapError(x => new Throwable(x.msg)) - - credentialWithEmbeddedProof <- emptyStatusListCredential.toJsonWithEmbeddedProof - } yield credentialWithEmbeddedProof.spaces2 + def updateLastUsedIndex(statusListId: UUID, lastUsedIndex: Int): ConnectionIO[Int] = + sql""" + | UPDATE public.credential_status_lists + | SET + | last_used_index = $lastUsedIndex, + | updated_at = ${Instant.now()} + | WHERE + | id = $statusListId + |""".stripMargin.update.run (for { - credentialStr <- credentialWithEmbeddedProof - query = sql""" - |INSERT INTO public.credential_status_lists ( - | id, - | issuer, - | issued, - | purpose, - | status_list_credential, - | size, - | last_used_index, - | wallet_id - | ) - |VALUES ( - | $id, - | $issuerDid, - | $issued, - | ${StatusPurpose.Revocation}::public.enum_credential_status_list_purpose, - | $credentialStr::JSON, - | ${BitString.MIN_SL2021_SIZE}, - | 0, - | current_setting('app.current_wallet_id')::UUID - | ) - |RETURNING id, wallet_id, issuer, issued, purpose, status_list_credential, size, last_used_index, created_at, updated_at - """.stripMargin.query[CredentialStatusList].unique - newStatusList <- query.transactWallet(xa) - } yield newStatusList).orDie - + id <- ZIO.succeed(UUID.randomUUID()) + newStatusListVC <- createStatusListVC(jwtIssuer, statusListRegistryUrl, id) + walletCtx <- ZIO.service[WalletAccessContext] + walletId = walletCtx.walletId + cnxIO = for { + _ <- acquireAdvisoryLock(walletId) + maybeStatusList <- getLatestOfTheWallet + statusList <- maybeStatusList match + case Some(csl) if csl.lastUsedIndex < csl.size => cats.free.Free.pure[ConnectionOp, CredentialStatusList](csl) + case _ => createNewForTheWallet(id, jwtIssuer.did.toString, Instant.now(), newStatusListVC) + newIndex = statusList.lastUsedIndex + 1 + _ <- updateLastUsedIndex(statusList.id, newIndex) + } yield (statusList.id, newIndex) + result <- cnxIO.transactWallet(xa) + } yield result).orDie } def allocateSpaceForCredential( @@ -214,9 +221,24 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T } yield () } - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] = { + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = { + val cxnIO = + sql""" + | SELECT + | wallet_id, + | id + | FROM public.credential_status_lists + |""".stripMargin + .query[(WalletId, UUID)] + .to[Seq] + cxnIO + .transact(xb) + .orDie + } - // Might need to add wallet Id in the select query, because I'm selecting all of them + def getCredentialStatusListsWithCreds( + statusListId: UUID + ): URIO[WalletAccessContext, CredentialStatusListWithCreds] = { val cxnIO = sql""" | SELECT @@ -235,42 +257,35 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T | cisl.is_processed | FROM public.credential_status_lists csl | LEFT JOIN public.credentials_in_status_list cisl ON csl.id = cisl.credential_status_list_id + | WHERE + | csl.id = $statusListId |""".stripMargin .query[CredentialStatusListWithCred] .to[List] - - val credentialStatusListsWithCredZio = cxnIO - .transact(xb) - .orDie - - for { - credentialStatusListsWithCred <- credentialStatusListsWithCredZio - } yield { - credentialStatusListsWithCred - .groupBy(_.credentialStatusListId) - .map { case (id, items) => - CredentialStatusListWithCreds( - id, - items.head.walletId, - items.head.issuer, - items.head.issued, - items.head.purpose, - items.head.statusListCredential, - items.head.size, - items.head.lastUsedIndex, - items.map { item => - CredInStatusList( - item.credentialInStatusListId, - item.issueCredentialRecordId, - item.statusListIndex, - item.isCanceled, - item.isProcessed, - ) - } + .transactWallet(xa) + .orDie + + cxnIO.map(items => + CredentialStatusListWithCreds( + statusListId, + items.head.walletId, + items.head.issuer, + items.head.issued, + items.head.purpose, + items.head.statusListCredential, + items.head.size, + items.head.lastUsedIndex, + items.map { item => + CredInStatusList( + item.credentialInStatusListId, + item.issueCredentialRecordId, + item.statusListIndex, + item.isCanceled, + item.isProcessed, ) } - .toList - } + ) + ) } def updateStatusListCredential( diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala index 1e477640c1..71b4cad3dc 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala @@ -459,7 +459,6 @@ class JdbcPresentationRepository( | id = $recordId | AND protocol_state = $from """.stripMargin.update - cxnIO.run .transactWallet(xa) .orDie diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala new file mode 100644 index 0000000000..8c3a60e56e --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala @@ -0,0 +1,106 @@ +package org.hyperledger.identus.shared.messaging + +import org.hyperledger.identus.shared.messaging.kafka.{InMemoryMessagingService, ZKafkaMessagingServiceImpl} +import zio.{durationInt, Cause, Duration, EnvironmentTag, RIO, RLayer, Task, URIO, URLayer, ZIO, ZLayer} + +import java.time.Instant +trait MessagingService { + def makeConsumer[K, V](groupId: String)(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] + def makeProducer[K, V]()(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] +} + +object MessagingService { + + case class RetryStep(topicName: String, consumerCount: Int, consumerBackoff: Duration, nextTopicName: Option[String]) + + object RetryStep { + def apply(topicName: String, consumerCount: Int, consumerBackoff: Duration, nextTopicName: String): RetryStep = + RetryStep(topicName, consumerCount, consumerBackoff, Some(nextTopicName)) + } + + def consumeWithRetryStrategy[K: EnvironmentTag, V: EnvironmentTag, HR]( + groupId: String, + handler: Message[K, V] => RIO[HR, Unit], + steps: Seq[RetryStep] + )(implicit kSerde: Serde[K], vSerde: Serde[V]): RIO[HR & Producer[K, V] & MessagingService, Unit] = { + for { + messagingService <- ZIO.service[MessagingService] + messageProducer <- ZIO.service[Producer[K, V]] + _ <- ZIO.foreachPar(steps) { step => + ZIO.foreachPar(1 to step.consumerCount)(_ => + for { + consumer <- messagingService.makeConsumer[K, V](groupId) + _ <- consumer + .consume[HR](step.topicName) { m => + for { + // Wait configured backoff before processing message + millisSpentInQueue <- ZIO.succeed(Instant.now().toEpochMilli - m.timestamp) + sleepDelay = step.consumerBackoff.toMillis - millisSpentInQueue + _ <- ZIO.when(sleepDelay > 0)(ZIO.sleep(Duration.fromMillis(sleepDelay))) + _ <- handler(m) + .catchAll { t => + for { + _ <- ZIO.logErrorCause(s"Error processing message: ${m.key} ", Cause.fail(t)) + _ <- step.nextTopicName match + case Some(name) => + messageProducer + .produce(name, m.key, m.value) + .catchAll(t => + ZIO.logErrorCause("Unable to send message to the next topic", Cause.fail(t)) + ) + case None => ZIO.unit + } yield () + } + .catchAllDefect(t => ZIO.logErrorCause(s"Defect processing message: ${m.key} ", Cause.fail(t))) + } yield () + } + .debug + .fork + } yield () + ) + } + } yield () + } + + def consume[K: EnvironmentTag, V: EnvironmentTag, HR]( + groupId: String, + topicName: String, + consumerCount: Int, + handler: Message[K, V] => RIO[HR, Unit] + )(implicit kSerde: Serde[K], vSerde: Serde[V]): RIO[HR & Producer[K, V] & MessagingService, Unit] = + consumeWithRetryStrategy(groupId, handler, Seq(RetryStep(topicName, consumerCount, 0.seconds, None))) + + val serviceLayer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer + .service[MessagingServiceConfig] + .flatMap(config => + if (config.get.kafkaEnabled) ZKafkaMessagingServiceImpl.layer + else InMemoryMessagingService.layer + ) + + def producerLayer[K: EnvironmentTag, V: EnvironmentTag](implicit + kSerde: Serde[K], + vSerde: Serde[V] + ): RLayer[MessagingService, Producer[K, V]] = ZLayer.fromZIO(for { + messagingService <- ZIO.service[MessagingService] + producer <- messagingService.makeProducer[K, V]() + } yield producer) + + def consumerLayer[K: EnvironmentTag, V: EnvironmentTag](groupId: String)(implicit + kSerde: Serde[K], + vSerde: Serde[V] + ): RLayer[MessagingService, Consumer[K, V]] = ZLayer.fromZIO(for { + messagingService <- ZIO.service[MessagingService] + consumer <- messagingService.makeConsumer[K, V](groupId) + } yield consumer) + +} + +case class Message[K, V](key: K, value: V, offset: Long, timestamp: Long) + +trait Consumer[K, V] { + def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] +} +trait Producer[K, V] { + def produce(topic: String, key: K, value: V): Task[Unit] +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala new file mode 100644 index 0000000000..dd63c1a424 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala @@ -0,0 +1,58 @@ +package org.hyperledger.identus.shared.messaging + +import zio.{ULayer, ZLayer} + +import java.time.Duration + +case class MessagingServiceConfig( + connectFlow: ConsumerJobConfig, + issueFlow: ConsumerJobConfig, + presentFlow: ConsumerJobConfig, + didStateSync: ConsumerJobConfig, + statusListSync: ConsumerJobConfig, + inMemoryQueueCapacity: Int, + kafkaEnabled: Boolean, + kafka: Option[KafkaConfig] +) + +final case class ConsumerJobConfig( + consumerCount: Int, + retryStrategy: Option[ConsumerRetryStrategy] +) + +final case class ConsumerRetryStrategy( + maxRetries: Int, + initialDelay: Duration, + maxDelay: Duration +) + +final case class KafkaConfig( + bootstrapServers: String, + consumers: KafkaConsumersConfig +) + +final case class KafkaConsumersConfig( + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) + +object MessagingServiceConfig { + + val inMemoryLayer: ULayer[MessagingServiceConfig] = + ZLayer.succeed( + MessagingServiceConfig( + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + 100, + false, + None + ) + ) + +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala new file mode 100644 index 0000000000..94eadf3849 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala @@ -0,0 +1,55 @@ +package org.hyperledger.identus.shared.messaging + +import org.hyperledger.identus.shared.models.WalletId + +import java.nio.charset.StandardCharsets +import java.nio.ByteBuffer +import java.util.UUID + +case class ByteArrayWrapper(ba: Array[Byte]) + +trait Serde[T] { + def serialize(t: T): Array[Byte] + def deserialize(ba: Array[Byte]): T +} + +object Serde { + given byteArraySerde: Serde[ByteArrayWrapper] = new Serde[ByteArrayWrapper] { + override def serialize(t: ByteArrayWrapper): Array[Byte] = t.ba + override def deserialize(ba: Array[Byte]): ByteArrayWrapper = ByteArrayWrapper(ba) + } + + given stringSerde: Serde[String] = new Serde[String] { + override def serialize(t: String): Array[Byte] = t.getBytes() + override def deserialize(ba: Array[Byte]): String = new String(ba, StandardCharsets.UTF_8) + } + + given intSerde: Serde[Int] = new Serde[Int] { + override def serialize(t: Int): Array[Byte] = { + val buffer = java.nio.ByteBuffer.allocate(4) + buffer.putInt(t) + buffer.array() + } + override def deserialize(ba: Array[Byte]): Int = ByteBuffer.wrap(ba).getInt() + } + + given uuidSerde: Serde[UUID] = new Serde[UUID] { + override def serialize(t: UUID): Array[Byte] = { + val buffer = java.nio.ByteBuffer.allocate(16) + buffer.putLong(t.getMostSignificantBits) + buffer.putLong(t.getLeastSignificantBits) + buffer.array() + } + override def deserialize(ba: Array[Byte]): UUID = { + val byteBuffer = ByteBuffer.wrap(ba) + val high = byteBuffer.getLong + val low = byteBuffer.getLong + new UUID(high, low) + } + } + + given walletIdSerde(using uuidSerde: Serde[UUID]): Serde[WalletId] = new Serde[WalletId] { + override def serialize(w: WalletId): Array[Byte] = uuidSerde.serialize(w.toUUID) + override def deserialize(ba: Array[Byte]): WalletId = WalletId.fromUUID(uuidSerde.deserialize(ba)) + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala new file mode 100644 index 0000000000..ff1c9e8d76 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala @@ -0,0 +1,20 @@ +package org.hyperledger.identus.shared.messaging + +import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} + +import java.nio.charset.StandardCharsets +import java.util.UUID + +case class WalletIdAndRecordId(walletId: UUID, recordId: UUID) + +object WalletIdAndRecordId { + given encoder: JsonEncoder[WalletIdAndRecordId] = DeriveJsonEncoder.gen[WalletIdAndRecordId] + given decoder: JsonDecoder[WalletIdAndRecordId] = DeriveJsonDecoder.gen[WalletIdAndRecordId] + given ser: Serde[WalletIdAndRecordId] = new Serde[WalletIdAndRecordId] { + override def serialize(t: WalletIdAndRecordId): Array[Byte] = t.toJson.getBytes(StandardCharsets.UTF_8) + override def deserialize(ba: Array[Byte]): WalletIdAndRecordId = + new String(ba, StandardCharsets.UTF_8) + .fromJson[WalletIdAndRecordId] + .getOrElse(throw RuntimeException("Deserialization Error WalletIdAndRecordId")) + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala new file mode 100644 index 0000000000..54d8c935c2 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala @@ -0,0 +1,146 @@ +package org.hyperledger.identus.shared.messaging.kafka + +import org.hyperledger.identus.shared.messaging.* +import org.hyperledger.identus.shared.messaging.kafka.InMemoryMessagingService.* +import zio.* +import zio.concurrent.ConcurrentMap +import zio.stream.* + +import java.util.concurrent.TimeUnit + +case class ConsumerGroupKey(groupId: GroupId, topic: Topic) + +class InMemoryMessagingService( + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + queueCapacity: Int, + processedMessagesMap: ConcurrentMap[ + ConsumerGroupKey, + ConcurrentMap[Offset, TimeStamp] + ] +) extends MessagingService { + + override def makeConsumer[K, V](groupId: String)(using kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] = { + ZIO.succeed(new InMemoryConsumer[K, V](groupId, topicQueues, processedMessagesMap)) + } + + override def makeProducer[K, V]()(using kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] = + ZIO.succeed(new InMemoryProducer[K, V](topicQueues, queueCapacity)) +} + +class InMemoryConsumer[K, V]( + groupId: GroupId, + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + processedMessagesMap: ConcurrentMap[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]] +) extends Consumer[K, V] { + override def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] = { + val allTopics = topic +: topics + def getQueueStream(topic: String): ZStream[Any, Nothing, (String, Message[K, V])] = + ZStream.repeatZIO { + topicQueues.get(topic).flatMap { + case Some((queue, _)) => + ZIO.debug(s"Connected to queue for topic $topic in group $groupId") *> + ZIO.succeed(ZStream.fromQueue(queue).collect { case msg: Message[K, V] @unchecked => (topic, msg) }) + case None => + ZIO.sleep(1.second) *> ZIO.succeed(ZStream.empty) + } + }.flatten + + val streams = allTopics.map(getQueueStream) + ZStream + .mergeAllUnbounded()(streams: _*) + .tap { case (topic, msg) => ZIO.log(s"Processing message in group $groupId, topic:$topic : $msg") } + .filterZIO { case (topic, msg) => + for { + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + key = ConsumerGroupKey(groupId, topic) + topicProcessedMessages <- processedMessagesMap.get(key).flatMap { + case Some(map) => ZIO.succeed(map) + case None => + for { + newMap <- ConcurrentMap.empty[Offset, TimeStamp] + _ <- processedMessagesMap.put(key, newMap) + } yield newMap + } + isNew <- topicProcessedMessages + .putIfAbsent(Offset(msg.offset), TimeStamp(currentTime)) + .map(_.isEmpty) + } yield isNew + } + .mapZIO { case (_, msg) => handler(msg) } + .tap(_ => ZIO.log(s"Message processed in group $groupId, topic:$topic")) + .runDrain + } +} + +class InMemoryProducer[K, V]( + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + queueCapacity: Int +) extends Producer[K, V] { + override def produce(topic: String, key: K, value: V): Task[Unit] = for { + queueAndOffsetRef <- topicQueues.get(topic).flatMap { + case Some(qAndOffSetRef) => ZIO.succeed(qAndOffSetRef) + case None => + for { + newQueue <- Queue.sliding[Message[_, _]](queueCapacity) + newOffSetRef <- Ref.make(Offset(0L)) + _ <- topicQueues.put(topic, (newQueue, newOffSetRef)) + } yield (newQueue, newOffSetRef) + } + (queue, offsetRef) = queueAndOffsetRef + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + messageId <- offsetRef.updateAndGet(x => Offset(x.value + 1)) // unique atomic id incremented per topic + _ <- queue.offer(Message(key, value, messageId.value, currentTime)) + } yield () +} + +object InMemoryMessagingService { + type Topic = String + type GroupId = String + + opaque type Offset = Long + object Offset: + def apply(value: Long): Offset = value + extension (id: Offset) def value: Long = id + + opaque type TimeStamp = Long + object TimeStamp: + def apply(value: Long): TimeStamp = value + extension (ts: TimeStamp) def value: Long = ts + + val layer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer.fromZIO { + for { + config <- ZIO.service[MessagingServiceConfig] + queueMap <- ConcurrentMap.empty[Topic, (Queue[Message[_, _]], Ref[Offset])] + processedMessagesMap <- ConcurrentMap.empty[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]] + _ <- cleanupTaskForProcessedMessages(processedMessagesMap) + } yield new InMemoryMessagingService(queueMap, config.inMemoryQueueCapacity, processedMessagesMap) + } + + private def cleanupTaskForProcessedMessages( + processedMessagesMap: ConcurrentMap[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]], + maxAge: Duration = 60.minutes // Maximum age for entries + ): UIO[Unit] = { + def cleanupOldEntries(map: ConcurrentMap[Offset, TimeStamp]): UIO[Unit] = for { + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + entries <- map.toList + _ <- ZIO.foreachDiscard(entries) { case (key, timestamp) => + if (currentTime - timestamp > maxAge.toMillis) + map.remove(key) *> ZIO.log(s"Removed old entry with key: $key and timestamp: $timestamp") + else + ZIO.unit + } + } yield () + + (for { + entries <- processedMessagesMap.toList + _ <- ZIO.foreachDiscard(entries) { case (key, map) => + ZIO.log(s"Cleaning up entries for group: ${key.groupId} and topic: ${key.topic}") *> + cleanupOldEntries(map) + } + } yield ()) + .repeat(Schedule.spaced(10.minutes)) + .fork + .unit + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala new file mode 100644 index 0000000000..9180fc4d62 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala @@ -0,0 +1,136 @@ +package org.hyperledger.identus.shared.messaging.kafka + +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.header.Headers +import org.hyperledger.identus.shared.messaging.* +import zio.{Duration, RIO, Task, URIO, URLayer, ZIO, ZLayer} +import zio.kafka.consumer.{ + Consumer as ZKConsumer, + ConsumerSettings as ZKConsumerSettings, + Subscription as ZKSubscription +} +import zio.kafka.producer.{Producer as ZKProducer, ProducerSettings as ZKProducerSettings} +import zio.kafka.serde.{Deserializer as ZKDeserializer, Serializer as ZKSerializer} + +class ZKafkaMessagingServiceImpl( + bootstrapServers: List[String], + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) extends MessagingService { + override def makeConsumer[K, V](groupId: String)(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] = + ZIO.succeed( + new ZKafkaConsumerImpl[K, V]( + bootstrapServers, + groupId, + kSerde, + vSerde, + autoCreateTopics, + maxPollRecords, + maxPollInterval, + pollTimeout, + rebalanceSafeCommits + ) + ) + + override def makeProducer[K, V]()(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] = + ZIO.succeed(new ZKafkaProducerImpl[K, V](bootstrapServers, kSerde, vSerde)) +} + +object ZKafkaMessagingServiceImpl { + val layer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer.fromZIO { + for { + config <- ZIO.service[MessagingServiceConfig] + kafkaConfig <- config.kafka match + case Some(cfg) => ZIO.succeed(cfg) + case None => ZIO.dieMessage("Kafka config is undefined") + } yield new ZKafkaMessagingServiceImpl( + kafkaConfig.bootstrapServers.split(',').toList, + kafkaConfig.consumers.autoCreateTopics, + kafkaConfig.consumers.maxPollRecords, + kafkaConfig.consumers.maxPollInterval, + kafkaConfig.consumers.pollTimeout, + kafkaConfig.consumers.rebalanceSafeCommits + ) + } +} + +class ZKafkaConsumerImpl[K, V]( + bootstrapServers: List[String], + groupId: String, + kSerde: Serde[K], + vSerde: Serde[V], + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) extends Consumer[K, V] { + private val zkConsumer = ZLayer.scoped( + ZKConsumer.make( + ZKConsumerSettings(bootstrapServers) + .withProperty(ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, autoCreateTopics.toString) + .withGroupId(groupId) + // 'max.poll.records' default is 500. This is a Kafka property. + .withMaxPollRecords(maxPollRecords) + // 'max.poll.interval.ms' default is 5 minutes. This is a Kafka property. + .withMaxPollInterval(maxPollInterval) // Should be max.poll.records x 'max processing time per record' + // 'pollTimeout' default is 50 millis. This is a ZIO Kafka property. + .withPollTimeout(pollTimeout) + // .withOffsetRetrieval(OffsetRetrieval.Auto(AutoOffsetStrategy.Earliest)) + .withRebalanceSafeCommits(rebalanceSafeCommits) + // .withMaxRebalanceDuration(30.seconds) + ) + ) + + private val zkKeyDeserializer = new ZKDeserializer[Any, K] { + override def deserialize(topic: String, headers: Headers, data: Array[Byte]): RIO[Any, K] = + ZIO.succeed(kSerde.deserialize(data)) + } + + private val zkValueDeserializer = new ZKDeserializer[Any, V] { + override def deserialize(topic: String, headers: Headers, data: Array[Byte]): RIO[Any, V] = + ZIO.succeed(vSerde.deserialize(data)) + } + + override def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] = + ZKConsumer + .plainStream(ZKSubscription.topics(topic, topics*), zkKeyDeserializer, zkValueDeserializer) + .provideSomeLayer(zkConsumer) + .mapZIO(record => + handler(Message(record.key, record.value, record.offset.offset, record.timestamp)).as(record.offset) + ) + .aggregateAsync(ZKConsumer.offsetBatches) + .mapZIO(_.commit) + .runDrain +} + +class ZKafkaProducerImpl[K, V](bootstrapServers: List[String], kSerde: Serde[K], vSerde: Serde[V]) + extends Producer[K, V] { + private val zkProducer = ZLayer.scoped( + ZKProducer.make( + ZKProducerSettings(bootstrapServers) + ) + ) + + private val zkKeySerializer = new ZKSerializer[Any, K] { + override def serialize(topic: String, headers: Headers, value: K): RIO[Any, Array[Byte]] = + ZIO.succeed(kSerde.serialize(value)) + } + + private val zkValueSerializer = new ZKSerializer[Any, V] { + override def serialize(topic: String, headers: Headers, value: V): RIO[Any, Array[Byte]] = + ZIO.succeed(vSerde.serialize(value)) + } + + override def produce(topic: String, key: K, value: V): Task[Unit] = + ZKProducer + .produce(topic, key, value, zkKeySerializer, zkValueSerializer) + .tap(metadata => ZIO.logInfo(s"Message produced: ${metadata.offset()}")) + .map(_ => ()) + .provideSome(zkProducer) + +} diff --git a/tests/integration-tests/src/test/resources/containers/agent.yml b/tests/integration-tests/src/test/resources/containers/agent.yml index 5d3048eea3..74bf287ada 100644 --- a/tests/integration-tests/src/test/resources/containers/agent.yml +++ b/tests/integration-tests/src/test/resources/containers/agent.yml @@ -42,6 +42,8 @@ services: REST_SERVICE_URL: POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: API_KEY_ENABLED: + STATUS_LIST_SYNC_TRIGGER_RECURRENCE_DELAY: 5 seconds + DID_STATE_SYNC_TRIGGER_RECURRENCE_DELAY: 5 seconds # Secret storage configuration SECRET_STORAGE_BACKEND: VAULT_ADDR: "http://host.docker.internal:${VAULT_HTTP_PORT}" @@ -52,9 +54,13 @@ services: KEYCLOAK_CLIENT_ID: KEYCLOAK_CLIENT_SECRET: KEYCLOAK_UMA_AUTO_UPGRADE_RPT: true # no configurable at the moment + # Kafka Messaging Service + DEFAULT_KAFKA_ENABLED: true depends_on: postgres: condition: service_healthy + init-kafka: + condition: service_healthy ports: - "${AGENT_DIDCOMM_PORT}:${AGENT_DIDCOMM_PORT}" - "${AGENT_HTTP_PORT}:${AGENT_HTTP_PORT}" @@ -72,3 +78,91 @@ services: # Extra hosts for Linux networking extra_hosts: - "host.docker.internal:host-gateway" + zookeeper: + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + # ports: + # - 22181:2181 + + kafka: + image: confluentinc/cp-kafka:latest + depends_on: + - zookeeper + # ports: + # - 29092:29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: false + healthcheck: + test: + [ + "CMD", + "kafka-topics", + "--list", + "--bootstrap-server", + "localhost:9092", + ] + interval: 5s + timeout: 10s + retries: 5 + + init-kafka: + image: confluentinc/cp-kafka:latest + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/sh", "-c"] + command: | + " + # blocks until kafka is reachable + kafka-topics --bootstrap-server kafka:9092 --list + echo -e 'Creating kafka topics' + + # Connect + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-DLQ --replication-factor 1 --partitions 1 + + # Issue + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-DLQ --replication-factor 1 --partitions 1 + + # Present + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-DLQ --replication-factor 1 --partitions 1 + + # DID Publication State Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state --replication-factor 1 --partitions 5 + + # Status List Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list --replication-factor 1 --partitions 5 + + tail -f /dev/null + " + healthcheck: + test: + [ + "CMD-SHELL", + "kafka-topics --bootstrap-server kafka:9092 --list | grep -q 'sync-status-list'", + ] + interval: 5s + timeout: 10s + retries: 5