All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pl.touk.nussknacker.ui.process.newdeployment.DeploymentService.scala Maven / Gradle / Ivy

There is a newer version: 1.18.1
Show newest version
package pl.touk.nussknacker.ui.process.newdeployment

import cats.Applicative
import cats.data.{EitherT, NonEmptyList}
import com.typesafe.scalalogging.LazyLogging
import db.util.DBIOActionInstances._
import pl.touk.nussknacker.engine.api.component.NodesDeploymentData
import pl.touk.nussknacker.engine.api.deployment._
import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId}
import pl.touk.nussknacker.engine.api.{ProcessVersion => RuntimeVersionData}
import pl.touk.nussknacker.engine.deployment.{DeploymentData, DeploymentId => LegacyDeploymentId}
import pl.touk.nussknacker.engine.newdeployment.DeploymentId
import pl.touk.nussknacker.restmodel.validation.ValidationResults.ValidationErrors
import pl.touk.nussknacker.security.Permission
import pl.touk.nussknacker.security.Permission.Permission
import pl.touk.nussknacker.ui.db.entity.ProcessVersionEntityData
import pl.touk.nussknacker.ui.process.ScenarioMetadata
import pl.touk.nussknacker.ui.process.deployment.DeploymentManagerDispatcher
import pl.touk.nussknacker.ui.process.deployment.LoggedUserConversions.LoggedUserOps
import pl.touk.nussknacker.ui.process.newdeployment.DeploymentEntityFactory.{DeploymentEntityData, WithModifiedAt}
import pl.touk.nussknacker.ui.process.newdeployment.DeploymentService._
import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, ScenarioMetadataRepository}
import pl.touk.nussknacker.ui.process.version.ScenarioGraphVersionService
import pl.touk.nussknacker.ui.security.api.LoggedUser

import java.sql.Timestamp
import java.time.Clock
import scala.concurrent.{ExecutionContext, Future}
import scala.language.higherKinds
import scala.util.control.NonFatal

// TODO: This class is a new version of deployment.DeploymentService. The problem with the old one is that
//       it joins multiple responsibilities like activity log (currently called "actions") and deployments management.
//       Also, because of the fact that periodic mechanism is build as a plug-in (DeploymentManager), some deployment related
//       operations (run now operation) is modeled as a CustomAction. Eventually, we should:
//       - Move periodic mechanism into to the designer's core
//       - Remove CustomAction
//       After we do this, we can remove legacy classes and fully switch to the new once.
class DeploymentService(
    scenarioMetadataRepository: ScenarioMetadataRepository,
    scenarioGraphVersionService: ScenarioGraphVersionService,
    deploymentRepository: DeploymentRepository,
    dmDispatcher: DeploymentManagerDispatcher,
    dbioRunner: DBIOActionRunner,
    clock: Clock
)(implicit ec: ExecutionContext)
    extends LazyLogging {

  def getDeploymentStatus(
      id: DeploymentId
  )(implicit loggedUser: LoggedUser): Future[Either[GetDeploymentStatusError, WithModifiedAt[DeploymentStatus]]] =
    (for {
      deploymentWithScenarioMetadata <- getDeploymentById(id)
      _ <- checkPermission[Future, GetDeploymentStatusError](
        user = loggedUser,
        category = deploymentWithScenarioMetadata.scenarioMetadata.processCategory,
        permission = Permission.Read
      )
    } yield deploymentWithScenarioMetadata.deployment.statusWithModifiedAt).value

  def runDeployment(command: RunDeploymentCommand): Future[Either[RunDeploymentError, DeploymentForeignKeys]] =
    (for {
      scenarioMetadata <- getScenarioMetadata(command)
      _ <- checkPermission[Future, RunDeploymentError](
        user = command.user,
        category = scenarioMetadata.processCategory,
        permission = Permission.Deploy
      )
      _ <- EitherT.cond[Future](!scenarioMetadata.isFragment, (), DeploymentOfFragmentError)
      _ <- EitherT.cond[Future](!scenarioMetadata.isArchived, (), DeploymentOfArchivedScenarioError)
      scenarioGraphVersion <- EitherT(
        scenarioGraphVersionService.getValidResolvedLatestScenarioGraphVersion(scenarioMetadata, command.user)
      ).leftMap[RunDeploymentError](error => ScenarioGraphValidationError(error.errors))
      _ <- validateUsingDeploymentManager(scenarioMetadata, scenarioGraphVersion, command.user)
      // We keep deployments metrics (used by counts mechanism) keyed by scenario name.
      // Because of that we can't run more than one deployment for scenario in a time.
      // TODO: We should key metrics by deployment id and remove this limitation
      // Saving of deployment is the final step before deployment request because we want to store only requested deployments
      _ <- saveDeploymentEnsuringNoConcurrentDeploymentsForScenario(command, scenarioMetadata)
      _ <- runDeploymentUsingDeploymentManagerAsync(scenarioMetadata, scenarioGraphVersion, command)
    } yield DeploymentForeignKeys(scenarioMetadata.id, scenarioGraphVersion.id)).value

  private def getScenarioMetadata(
      command: RunDeploymentCommand
  ): EitherT[Future, RunDeploymentError, ScenarioMetadata] =
    EitherT.fromOptionF(
      dbioRunner.run(scenarioMetadataRepository.getScenarioMetadata(command.scenarioName)),
      ScenarioNotFoundError(command.scenarioName)
    )

  private def saveDeploymentEnsuringNoConcurrentDeploymentsForScenario(
      command: RunDeploymentCommand,
      scenarioMetadata: ScenarioMetadata
  ): EitherT[Future, RunDeploymentError, Unit] = {
    EitherT(dbioRunner.runInSerializableTransactionWithRetry((for {
      nonFinishedDeployments <- getConcurrentlyPerformedDeploymentsForScenario(scenarioMetadata)
      _                      <- checkNoConcurrentDeploymentsForScenario(nonFinishedDeployments, scenarioMetadata.name)
      _ = {
        logger.debug(s"Saving deployment: ${command.id}")
      }
      _ <- saveDeployment(command, scenarioMetadata)
    } yield ()).value))
  }

  private def getConcurrentlyPerformedDeploymentsForScenario(scenarioMetadata: ScenarioMetadata) = {
    val nonPerformingDeploymentStatuses =
      Set(DeploymentStatus.Canceled.name, DeploymentStatus.Finished.name, ProblemDeploymentStatus.name)
    EitherT.right(
      deploymentRepository.getScenarioDeploymentsInNotMatchingStatus(
        scenarioMetadata.id,
        nonPerformingDeploymentStatuses
      )
    )
  }

  private def checkNoConcurrentDeploymentsForScenario(
      nonFinishedDeployments: Seq[DeploymentEntityData],
      scenarioName: ProcessName
  ) = {
    EitherT.fromEither(
      NonEmptyList
        .fromList(nonFinishedDeployments.toList)
        .map(conflictingDeployments =>
          Left(ConcurrentDeploymentsForScenarioArePerformedError(scenarioName, conflictingDeployments.map(_.id)))
        )
        .getOrElse(Right(()))
    )
  }

  private def saveDeployment(
      command: RunDeploymentCommand,
      scenarioMetadata: ScenarioMetadata
  ): EitherT[DB, RunDeploymentError, Unit] = {
    val now = Timestamp.from(clock.instant())
    EitherT(
      deploymentRepository.saveDeployment(
        DeploymentEntityData(
          command.id,
          scenarioMetadata.id,
          now,
          command.user.id,
          WithModifiedAt(DeploymentStatus.DuringDeploy, now)
        )
      )
    ).leftMap(e => ConflictingDeploymentIdError(e.id))
  }

  private def validateUsingDeploymentManager(
      scenarioMetadata: ScenarioMetadata,
      scenarioGraphVersion: ProcessVersionEntityData,
      user: LoggedUser
  ): EitherT[Future, RunDeploymentError, Unit] = {
    val runtimeVersionData = processVersionFor(scenarioMetadata, scenarioGraphVersion)
    // TODO: It shouldn't be needed
    val dumbDeploymentData = DeploymentData(
      LegacyDeploymentId(""),
      user.toManagerUser,
      Map.empty,
      NodesDeploymentData.empty
    )
    for {
      result <- EitherT[Future, RunDeploymentError, Unit](
        dmDispatcher
          .deploymentManagerUnsafe(scenarioMetadata.processingType)(user)
          .processCommand(
            DMValidateScenarioCommand(
              runtimeVersionData,
              dumbDeploymentData,
              scenarioGraphVersion.jsonUnsafe,
              DeploymentUpdateStrategy.DontReplaceDeployment
            )
          )
          .map(_ => Right(()))
          // TODO: more explicit way to pass errors from DM
          .recover { case NonFatal(ex) =>
            Left(DeployValidationError(ex.getMessage))
          }
      )
    } yield result
  }

  private def runDeploymentUsingDeploymentManagerAsync(
      scenarioMetadata: ScenarioMetadata,
      scenarioGraphVersion: ProcessVersionEntityData,
      command: RunDeploymentCommand
  ): EitherT[Future, RunDeploymentError, Unit] = {
    val runtimeVersionData = processVersionFor(scenarioMetadata, scenarioGraphVersion)
    val deploymentData = DeploymentData(
      toLegacyDeploymentId(command.id),
      command.user.toManagerUser,
      additionalDeploymentData = Map.empty,
      command.nodesDeploymentData
    )
    dmDispatcher
      .deploymentManagerUnsafe(scenarioMetadata.processingType)(command.user)
      .processCommand(
        DMRunDeploymentCommand(
          runtimeVersionData,
          deploymentData,
          scenarioGraphVersion.jsonUnsafe,
          DeploymentUpdateStrategy.DontReplaceDeployment
        )
      )
      .map { externalDeploymentId =>
        logger.debug(
          s"Deployment [${command.id}] successfully requested. External deployment id is: $externalDeploymentId"
        )
      }
      .failed
      .foreach(handleFailureDuringDeploymentRequesting(command.id, _))
    EitherT.pure(())
  }

  private def handleFailureDuringDeploymentRequesting(
      deploymentId: DeploymentId,
      ex: Throwable
  ): Unit = {
    logger.warn(s"Deployment [$deploymentId] requesting finished with failure. Status will be marked as PROBLEM", ex)
    dbioRunner
      .run(
        deploymentRepository.updateDeploymentStatus(
          deploymentId,
          DeploymentStatus.Problem.FailureDuringDeploymentRequesting
        )
      )
      .failed
      .foreach { ex =>
        logger.warn(s"Exception during marking deployment [$deploymentId] status as PROBLEM", ex)
      }
  }

  private def toLegacyDeploymentId(id: DeploymentId) = {
    LegacyDeploymentId(id.toString)
  }

  private def getDeploymentById(
      id: DeploymentId
  ): EitherT[Future, GetDeploymentStatusError, DeploymentRepository.DeploymentWithScenarioMetadata] =
    EitherT.fromOptionF(dbioRunner.run(deploymentRepository.getDeploymentById(id)), DeploymentNotFoundError(id))

  private def checkPermission[F[_]: Applicative, Error >: NoPermissionError.type](
      user: LoggedUser,
      category: String,
      permission: Permission
  ): EitherT[F, Error, Unit] =
    EitherT.cond[F](user.can(category, permission), (), NoPermissionError)

  private def processVersionFor(scenarioMetadata: ScenarioMetadata, scenarioGraphVersion: ProcessVersionEntityData) = {
    RuntimeVersionData(
      versionId = scenarioGraphVersion.id,
      processName = scenarioMetadata.name,
      processId = scenarioMetadata.id,
      labels = scenarioMetadata.labels.map(_.value),
      user = scenarioGraphVersion.user,
      modelVersion = scenarioGraphVersion.modelVersion
    )
  }

}

object DeploymentService {

  final case class DeploymentForeignKeys(scenarioId: ProcessId, scenarioGraphVersionId: VersionId)

  sealed trait RunDeploymentError

  sealed trait GetDeploymentStatusError

  final case class ConflictingDeploymentIdError(id: DeploymentId) extends RunDeploymentError

  final case class ConcurrentDeploymentsForScenarioArePerformedError(
      scenarioName: ProcessName,
      concurrentDeploymentsIds: NonEmptyList[DeploymentId]
  ) extends RunDeploymentError

  final case class ScenarioNotFoundError(scenarioName: ProcessName) extends RunDeploymentError

  case object DeploymentOfFragmentError extends RunDeploymentError

  case object DeploymentOfArchivedScenarioError extends RunDeploymentError

  final case class DeploymentNotFoundError(id: DeploymentId) extends GetDeploymentStatusError

  case object NoPermissionError extends RunDeploymentError with GetDeploymentStatusError

  final case class ScenarioGraphValidationError(errors: ValidationErrors) extends RunDeploymentError

  final case class DeployValidationError(message: String) extends RunDeploymentError

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy