From fcc4886b7442acc1b48c12d54a67da088627c9f5 Mon Sep 17 00:00:00 2001 From: Khalid Shakir Date: Thu, 5 Jan 2017 16:08:02 -0500 Subject: [PATCH 01/84] Version bump --- project/Version.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Version.scala b/project/Version.scala index 3cd7d5da8..a4dc20d69 100644 --- a/project/Version.scala +++ b/project/Version.scala @@ -4,7 +4,7 @@ import sbt._ object Version { // Upcoming release, or current if we're on a master / hotfix branch - val cromwellVersion = "24" + val cromwellVersion = "25" // Adapted from SbtGit.versionWithGit def cromwellVersionWithGit: Seq[Setting[_]] = From 864ff61c918b3bd7f17416015f1b967af8951e74 Mon Sep 17 00:00:00 2001 From: Khalid Shakir Date: Thu, 5 Jan 2017 16:09:29 -0500 Subject: [PATCH 02/84] Version bump in CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b720f39..a12ac4c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Cromwell Change Log +## 25 + + + ## 24 * When emitting workflow outputs to the Cromwell log only the first 1000 characters per output will be printed From 50af6d829c4819976c08cf202a92ac799aad2bbc Mon Sep 17 00:00:00 2001 From: Khalid Shakir Date: Wed, 28 Dec 2016 20:37:18 -0500 Subject: [PATCH 03/84] Moved files for next edits. --- .../backend/standard/StandardValidatedRuntimeAttributesBuilder.scala | 0 .../backend/validation}/PrimitiveRuntimeAttributesValidation.scala | 0 .../backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemValidatedRuntimeAttributesBuilder.scala => backend/src/main/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilder.scala (100%) rename {supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config => backend/src/main/scala/cromwell/backend/validation}/PrimitiveRuntimeAttributesValidation.scala (100%) rename supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemValidatedRuntimeAttributesBuilderSpec.scala => backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala (100%) diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemValidatedRuntimeAttributesBuilder.scala b/backend/src/main/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilder.scala similarity index 100% rename from supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemValidatedRuntimeAttributesBuilder.scala rename to backend/src/main/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilder.scala diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/PrimitiveRuntimeAttributesValidation.scala b/backend/src/main/scala/cromwell/backend/validation/PrimitiveRuntimeAttributesValidation.scala similarity index 100% rename from supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/PrimitiveRuntimeAttributesValidation.scala rename to backend/src/main/scala/cromwell/backend/validation/PrimitiveRuntimeAttributesValidation.scala diff --git a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemValidatedRuntimeAttributesBuilderSpec.scala b/backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala similarity index 100% rename from supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemValidatedRuntimeAttributesBuilderSpec.scala rename to backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala From a906fe6cbd5e03d375d54504a7a69fb0672d42b7 Mon Sep 17 00:00:00 2001 From: Khalid Shakir Date: Fri, 23 Dec 2016 00:34:29 -0500 Subject: [PATCH 04/84] Updates for Standard backends. JES now uses the uncustomized sync standard actor. Moved primitive runtime attributes validators to standard. Moved the SFS ValidatedRuntimeAttributesBuilder (VRAB) to standard. Moved the SFS initialization data to standard. JES and Standard backends now use VRAB within their backends via initialization data. Primitive runtime attribute validations now handle basic `WdlString` conversion to Boolean/Float/Integer. Merged code for continue on return code expression validation, from BackendWorkflowInitializationActor and ContinueOnReturnCodeValidation. Added a BackendWorkflowInitializationActorSpec. Updated ContinueOnReturnCodeValidation to also allow boolean strings `"true"` and `"false"`. The standard async backend pulls failOnStdErr and continueOnReturnCode from those generated by the VRAB. JesRuntimeAttributes (JRA) generation now extracts values from the the map build by the the VRAB. Realized based on the JRA Spec that the VRAB failure messages need to differentiate missing vs. bad. When encountering invalid runtime attributes, the validation should (optionally) say what the bad value is (see Jes' NoAddressValidation). Plumbed the synchronous actor of the job actor method params into the param-traits used by Standard. Moved the SFS path builders from the SFS factory to the SFS init actor. New standard cache-hit-copying actor trait, plus consolidation of the cache-hit-copying and async actors into a new standard helper trait. SharedFileSystemJob.JobIdKey is now SharedFileSystemAsyncJobExecutionActor.JobIdKey. WorkflowPaths.toJobPaths now takes into account that jobs for sub workflow use a different workflow descriptor than the workflow. Merged WorkflowPathsBackendInitializationData into StandardInitializationData. New standard finalization trait. Changed JES runtime attributes (JRA) to always require docker. Updated JRA spec match new required docker. While updating spec, changed JRA spec to match the primitive validator messages. --- .../BackendWorkflowInitializationActor.scala | 34 +--- .../scala/cromwell/backend/io/WorkflowPaths.scala | 23 ++- .../WorkflowPathsBackendInitializationData.scala | 44 ---- .../backend/io/WorkflowPathsWithDocker.scala | 14 +- .../standard/StandardAsyncExecutionActor.scala | 67 ++----- .../standard/StandardCacheHitCopyingActor.scala | 46 +++++ .../standard/StandardCachingActorHelper.scala | 89 +++++++++ .../standard/StandardFinalizationActor.scala | 41 ++++ .../standard/StandardInitializationActor.scala | 70 +++++++ .../standard/StandardInitializationData.scala | 21 ++ .../standard/StandardJobExecutionActorParams.scala | 19 +- .../standard/StandardLifecycleActorFactory.scala | 119 ++++++++++- .../standard/StandardSyncExecutionActor.scala | 1 + ...StandardValidatedRuntimeAttributesBuilder.scala | 101 +++------- .../ContinueOnReturnCodeValidation.scala | 49 +++-- .../backend/validation/CpuValidation.scala | 39 ++-- .../backend/validation/DockerValidation.scala | 27 +-- .../validation/FailOnStderrValidation.scala | 41 +--- .../backend/validation/MemoryValidation.scala | 27 ++- .../PrimitiveRuntimeAttributesValidation.scala | 63 ++++-- .../validation/RuntimeAttributesValidation.scala | 183 +++++++++-------- .../ValidatedRuntimeAttributesBuilder.scala | 27 +-- .../BackendWorkflowInitializationActorSpec.scala | 204 +++++++++++++++++++ ...dardValidatedRuntimeAttributesBuilderSpec.scala | 35 ++-- .../jes/JesAsyncBackendJobExecutionActor.scala | 49 +---- .../impl/jes/JesBackendInitializationData.scala | 10 +- .../impl/jes/JesBackendLifecycleActorFactory.scala | 92 +++------ .../backend/impl/jes/JesCacheHitCopyingActor.scala | 31 +-- .../backend/impl/jes/JesFinalizationActor.scala | 42 ++-- .../backend/impl/jes/JesInitializationActor.scala | 80 +++----- .../impl/jes/JesJobCachingActorHelper.scala | 47 ++--- .../backend/impl/jes/JesJobExecutionActor.scala | 61 ------ .../backend/impl/jes/JesPipelineInfo.scala | 11 +- .../backend/impl/jes/JesRuntimeAttributes.scala | 221 +++++++++++---------- .../backend/impl/jes/JesWorkflowPaths.scala | 22 +- .../backend/impl/jes/io/JesAttachedDisk.scala | 10 +- .../jes/JesAsyncBackendJobExecutionActorSpec.scala | 23 ++- .../impl/jes/JesInitializationActorSpec.scala | 18 +- .../impl/jes/JesJobExecutionActorSpec.scala | 29 +-- .../impl/jes/JesRuntimeAttributesSpec.scala | 44 ++-- .../sfs/config/ConfigAsyncJobExecutionActor.scala | 2 +- .../ConfigBackendLifecycleActorFactory.scala | 13 +- .../impl/sfs/config/ConfigHashingStrategy.scala | 17 +- .../sfs/config/ConfigInitializationActor.scala | 13 +- .../impl/sfs/config/DeclarationValidation.scala | 26 ++- .../SharedFileSystemAsyncJobExecutionActor.scala | 51 ++--- ...redFileSystemBackendLifecycleActorFactory.scala | 57 +----- .../sfs/SharedFileSystemCacheHitCopyingActor.scala | 24 +-- .../sfs/SharedFileSystemExpressionFunctions.scala | 13 +- .../sfs/SharedFileSystemInitializationActor.scala | 101 ++++------ .../SharedFileSystemJobCachingActorHelper.scala | 35 +--- .../sfs/config/ConfigHashingStrategySpec.scala | 4 +- .../SharedFileSystemInitializationActorSpec.scala | 9 +- .../SharedFileSystemJobExecutionActorSpec.scala | 5 +- .../sfs/TestLocalAsyncJobExecutionActor.scala | 12 +- 55 files changed, 1345 insertions(+), 1211 deletions(-) delete mode 100644 backend/src/main/scala/cromwell/backend/io/WorkflowPathsBackendInitializationData.scala create mode 100644 backend/src/main/scala/cromwell/backend/standard/StandardCacheHitCopyingActor.scala create mode 100644 backend/src/main/scala/cromwell/backend/standard/StandardCachingActorHelper.scala create mode 100644 backend/src/main/scala/cromwell/backend/standard/StandardFinalizationActor.scala create mode 100644 backend/src/main/scala/cromwell/backend/standard/StandardInitializationActor.scala create mode 100644 backend/src/main/scala/cromwell/backend/standard/StandardInitializationData.scala create mode 100644 backend/src/test/scala/cromwell/backend/BackendWorkflowInitializationActorSpec.scala delete mode 100644 supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobExecutionActor.scala diff --git a/backend/src/main/scala/cromwell/backend/BackendWorkflowInitializationActor.scala b/backend/src/main/scala/cromwell/backend/BackendWorkflowInitializationActor.scala index feaf5720b..626674f19 100644 --- a/backend/src/main/scala/cromwell/backend/BackendWorkflowInitializationActor.scala +++ b/backend/src/main/scala/cromwell/backend/BackendWorkflowInitializationActor.scala @@ -4,13 +4,14 @@ import akka.actor.{ActorLogging, ActorRef} import akka.event.LoggingReceive import cromwell.backend.BackendLifecycleActor._ import cromwell.backend.BackendWorkflowInitializationActor._ -import wdl4s.expression.PureStandardLibraryFunctions +import cromwell.backend.validation.ContinueOnReturnCodeValidation import cromwell.core.{WorkflowMetadataKeys, WorkflowOptions} import cromwell.services.metadata.MetadataService.PutMetadataAction import cromwell.services.metadata.{MetadataEvent, MetadataKey, MetadataValue} -import wdl4s.types._ -import wdl4s.values.{WdlArray, WdlBoolean, WdlInteger, WdlString, WdlValue} import wdl4s._ +import wdl4s.expression.PureStandardLibraryFunctions +import wdl4s.types._ +import wdl4s.values.WdlValue import scala.concurrent.Future import scala.util.{Failure, Success, Try} @@ -34,7 +35,7 @@ object BackendWorkflowInitializationActor { * Workflow-level actor for executing, recovering and aborting jobs. */ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor with ActorLogging { - val serviceRegistryActor: ActorRef + def serviceRegistryActor: ActorRef def calls: Set[TaskCall] @@ -67,26 +68,7 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w * return `true` in both cases. */ protected def continueOnReturnCodePredicate(valueRequired: Boolean)(wdlExpressionMaybe: Option[WdlValue]): Boolean = { - def isInteger(s: String) = s.forall(_.isDigit) && !s.isEmpty - - def validateValue(wdlValue: WdlValue) = wdlValue match { - case WdlInteger(_) => true - case WdlString(_) => isInteger(wdlValue.valueString) - case WdlArray(WdlArrayType(WdlIntegerType), _) => true - case WdlArray(WdlArrayType(WdlStringType), elements) => elements forall { x => isInteger(x.valueString) } - case WdlBoolean(_) => true - case _ => false - } - - wdlExpressionMaybe match { - case None => !valueRequired - case Some(wdlExpression: WdlExpression) => - wdlExpression.evaluate(NoLookup, PureStandardLibraryFunctions) match { - case Success(wdlValue) => validateValue(wdlValue) - case Failure(throwable) => true // If we can't evaluate it, we'll let it pass for now... - } - case Some(wdlValue) => validateValue(wdlValue) - } + ContinueOnReturnCodeValidation.default.validateOptionalExpression(wdlExpressionMaybe) } protected def runtimeAttributeValidators: Map[String, Option[WdlValue] => Boolean] @@ -94,7 +76,7 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w // FIXME: If a workflow executes jobs using multiple backends, // each backend will try to write its own workflow root and override any previous one. // They should be structured differently or at least be prefixed by the backend name - protected def publishWorkflowRoot(workflowRoot: String) = { + protected def publishWorkflowRoot(workflowRoot: String): Unit = { serviceRegistryActor ! PutMetadataAction(MetadataEvent(MetadataKey(workflowDescriptor.id, None, WorkflowMetadataKeys.WorkflowRoot), MetadataValue(workflowRoot))) } @@ -167,7 +149,7 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w /** * Our predefined sequence to run during preStart */ - final def initSequence() = for { + final def initSequence(): Future[InitializationSuccess] = for { _ <- validateRuntimeAttributes _ <- validate() data <- beforeAll() diff --git a/backend/src/main/scala/cromwell/backend/io/WorkflowPaths.scala b/backend/src/main/scala/cromwell/backend/io/WorkflowPaths.scala index a342d443e..b1955c9ec 100644 --- a/backend/src/main/scala/cromwell/backend/io/WorkflowPaths.scala +++ b/backend/src/main/scala/cromwell/backend/io/WorkflowPaths.scala @@ -3,7 +3,7 @@ package cromwell.backend.io import java.nio.file.Path import com.typesafe.config.Config -import cromwell.backend.{BackendJobDescriptorKey, BackendWorkflowDescriptor} +import cromwell.backend.{BackendJobDescriptor, BackendJobDescriptorKey, BackendWorkflowDescriptor} import cromwell.core.WorkflowOptions.FinalCallLogsDir import cromwell.core.path.{DefaultPathBuilder, PathFactory} import net.ceedubs.ficus.Ficus._ @@ -17,11 +17,11 @@ object WorkflowPaths { trait WorkflowPaths extends PathFactory { def workflowDescriptor: BackendWorkflowDescriptor def config: Config - + protected lazy val executionRootString: String = config.as[Option[String]]("root").getOrElse("cromwell-executions") - + def getPath(url: String): Try[Path] = Try(PathFactory.buildPath(url, pathBuilders)) - + // Rebuild potential intermediate call directories in case of a sub workflow protected def workflowPathBuilder(root: Path): Path = { workflowDescriptor.breadCrumbs.foldLeft(root)((acc, breadCrumb) => { @@ -34,5 +34,18 @@ trait WorkflowPaths extends PathFactory { lazy val finalCallLogsPath: Option[Path] = workflowDescriptor.getWorkflowOption(FinalCallLogsDir) map getPath map { _.get } - def toJobPaths(jobKey: BackendJobDescriptorKey): JobPaths + def toJobPaths(jobDescriptor: BackendJobDescriptor): JobPaths = { + toJobPaths(jobDescriptor.key, jobDescriptor.workflowDescriptor) + } + + /** + * Creates job paths using the key and workflow descriptor. + * + * NOTE: For sub workflows, the jobWorkflowDescriptor will be different than the WorkflowPaths.workflowDescriptor. + * + * @param jobKey The key for the job. + * @param jobWorkflowDescriptor The workflow descriptor for the job. + * @return The paths for the job. + */ + def toJobPaths(jobKey: BackendJobDescriptorKey, jobWorkflowDescriptor: BackendWorkflowDescriptor): JobPaths } diff --git a/backend/src/main/scala/cromwell/backend/io/WorkflowPathsBackendInitializationData.scala b/backend/src/main/scala/cromwell/backend/io/WorkflowPathsBackendInitializationData.scala deleted file mode 100644 index b0861d6bb..000000000 --- a/backend/src/main/scala/cromwell/backend/io/WorkflowPathsBackendInitializationData.scala +++ /dev/null @@ -1,44 +0,0 @@ -package cromwell.backend.io - -import cromwell.backend.BackendInitializationData -import cromwell.core.path.PathBuilder - -/** - * Extension of backend initialization data that also provides a `WorkflowPaths`, and by proxy its `List[FileSystem]`. - * - * This class consolidates code used by the legacy Local backend and the Shared File System (SFS) backend, that need to - * localize data from a collection of different file systems (local and GCS for now). - * - * NOTE: The JES backend uses a fork of the `WorkflowPaths`. As `JesWorkflowPaths` is not a subclass, the - * initialization data for JES does not use this trait nor the companion object. It is not clear if JES needs the - * functionality, as it cannot localize from generic file systems, only GCS with different authentication modes. - * - * Meanwhile the local and SFS backends need access to the functionality in workflow paths. Their initialization data - * will therefore be a subset of this trait. - * - * The initialization data will be generated in the intialization actor as class extending this trait. - * - * From there, the initialization actor will pass the data back to the backend factory, wrapped in an Option, with its - * type downcast to an `Option[BackendInitializationData]`. - * - * The factory then sends a copy of the intialization data down to each execution actor created for every `wdl4s.Call`. - * - * Each instance of the execution actor will unpack the initialization data, obtaining the workflow paths using - * `WorkflowPathsBackendInitializationData.workflowPaths(Option[BackendInitializationData]): WorkflowPaths` - * - * However, the execution instances are most interested in actually getting to the file systems embedded within the - * workflow paths. That functionality is provided via `WorkflowPathsBackendInitializationData.workflowPaths` - */ -trait WorkflowPathsBackendInitializationData extends BackendInitializationData { - def workflowPaths: WorkflowPaths -} - -object WorkflowPathsBackendInitializationData { - def workflowPaths(initializationDataOption: Option[BackendInitializationData]): WorkflowPaths = { - BackendInitializationData.as[WorkflowPathsBackendInitializationData](initializationDataOption).workflowPaths - } - - def pathBuilders(initializationDataOption: Option[BackendInitializationData]): List[PathBuilder] = { - workflowPaths(initializationDataOption).pathBuilders - } -} diff --git a/backend/src/main/scala/cromwell/backend/io/WorkflowPathsWithDocker.scala b/backend/src/main/scala/cromwell/backend/io/WorkflowPathsWithDocker.scala index c10e66972..26e6ce95d 100644 --- a/backend/src/main/scala/cromwell/backend/io/WorkflowPathsWithDocker.scala +++ b/backend/src/main/scala/cromwell/backend/io/WorkflowPathsWithDocker.scala @@ -1,16 +1,20 @@ package cromwell.backend.io -import java.nio.file.Paths +import java.nio.file.{Path, Paths} import com.typesafe.config.Config import cromwell.backend.{BackendJobDescriptorKey, BackendWorkflowDescriptor} import cromwell.core.path.PathBuilder object WorkflowPathsWithDocker { - val DockerRoot = Paths.get("/root") + val DockerRoot: Path = Paths.get("/root") } class WorkflowPathsWithDocker(val workflowDescriptor: BackendWorkflowDescriptor, val config: Config, val pathBuilders: List[PathBuilder] = WorkflowPaths.DefaultPathBuilders) extends WorkflowPaths { - val dockerWorkflowRoot = workflowPathBuilder(WorkflowPathsWithDocker.DockerRoot) - override def toJobPaths(jobKey: BackendJobDescriptorKey): JobPaths = new JobPathsWithDocker(jobKey, workflowDescriptor, config, pathBuilders) -} \ No newline at end of file + val dockerWorkflowRoot: Path = workflowPathBuilder(WorkflowPathsWithDocker.DockerRoot) + + override def toJobPaths(jobKey: BackendJobDescriptorKey, + jobWorkflowDescriptor: BackendWorkflowDescriptor): JobPathsWithDocker = { + new JobPathsWithDocker(jobKey, jobWorkflowDescriptor, config, pathBuilders) + } +} diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala b/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala index daabf40a1..a88f07fdb 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala @@ -1,7 +1,5 @@ package cromwell.backend.standard -import java.nio.file.Path - import akka.actor.{Actor, ActorLogging, ActorRef} import akka.event.LoggingReceive import better.files.File @@ -9,7 +7,7 @@ import cromwell.backend.BackendJobExecutionActor.{AbortedResponse, BackendJobExe import cromwell.backend.BackendLifecycleActor.AbortJobCommand import cromwell.backend.async.AsyncBackendJobExecutionActor.{ExecutionMode, JobId, Recover} import cromwell.backend.async.{AbortedExecutionHandle, AsyncBackendJobExecutionActor, ExecutionHandle, FailedNonRetryableExecutionHandle, PendingExecutionHandle, SuccessfulExecutionHandle} -import cromwell.backend.validation.{ContinueOnReturnCode, ContinueOnReturnCodeFlag} +import cromwell.backend.validation._ import cromwell.backend.wdl.Command import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, BackendJobDescriptor, BackendJobLifecycleActor} import cromwell.services.keyvalue.KeyValueServiceActor._ @@ -31,7 +29,7 @@ import scala.util.{Failure, Success, Try} * NOTE: Unlike the parent trait `AsyncBackendJobExecutionActor`, this trait is subject to even more frequent updates * as the common behavior among the backends adjusts in unison. */ -trait StandardAsyncExecutionActor extends AsyncBackendJobExecutionActor { +trait StandardAsyncExecutionActor extends AsyncBackendJobExecutionActor with StandardCachingActorHelper { this: Actor with ActorLogging with BackendJobLifecycleActor => val SIGTERM = 143 @@ -48,26 +46,23 @@ trait StandardAsyncExecutionActor extends AsyncBackendJobExecutionActor { PendingExecutionHandle[StandardAsyncJob, StandardAsyncRunInfo, StandardAsyncRunStatus] /** Standard set of parameters passed to the backend. */ - val standardParams: StandardAsyncExecutionActorParams - - override lazy val jobDescriptor: BackendJobDescriptor = standardParams.jobDescriptor + def standardParams: StandardAsyncExecutionActorParams override lazy val configurationDescriptor: BackendConfigurationDescriptor = standardParams.configurationDescriptor override lazy val completionPromise: Promise[BackendJobExecutionResponse] = standardParams.completionPromise /** Backend initialization data created by the a factory initializer. */ - lazy val backendInitializationDataOption: Option[BackendInitializationData] = + override lazy val backendInitializationDataOption: Option[BackendInitializationData] = standardParams.backendInitializationDataOption - /** Typed backend initialization. */ - def backendInitializationDataAs[A <: BackendInitializationData]: A = - BackendInitializationData.as[A](backendInitializationDataOption) + /** @see [[StandardAsyncExecutionActorParams.serviceRegistryActor]] */ + override lazy val serviceRegistryActor: ActorRef = standardParams.serviceRegistryActor - /** @see [[StandardJobExecutionActorParams.serviceRegistryActor]] */ - lazy val serviceRegistryActor: ActorRef = standardParams.serviceRegistryActor + /** @see [[StandardAsyncExecutionActorParams.jobDescriptor]] */ + override lazy val jobDescriptor: BackendJobDescriptor = standardParams.jobDescriptor - /** @see [[StandardJobExecutionActorParams.jobIdKey]] */ + /** @see [[StandardAsyncExecutionActorParams.jobIdKey]] */ def jobIdKey: String = standardParams.jobIdKey /** @see [[Command.instantiate]] */ @@ -91,48 +86,16 @@ trait StandardAsyncExecutionActor extends AsyncBackendJobExecutionActor { * * @return True if a non-empty `remoteStdErrPath` should fail the job. */ - def failOnStdErr: Boolean = false - - /** - * Returns the path to the standard error output of the job. Only needs to be implemented if `failOnStdErr` is - * returning `true`. - * - * @return The path to the standard error output. - */ - def remoteStdErrPath: Path = { - throw new NotImplementedError(s"failOnStdErr returned true but remote path not implemented by $getClass") - } - - /** - * Returns the path to the return code output of the job. Must be implemented unless `returnCodeContents` is - * overridden not to use this method. - * - * @return The path to the return code output. - */ - def remoteReturnCodePath: Path = { - throw new NotImplementedError(s"remoteReturnCodePath returned true but remote path not implemented by $getClass") - } - - /** - * Returns the contents of the return code file. - * - * @return The contents of the return code file. - */ - def returnCodeContents: String = File(remoteReturnCodePath).contentAsString + lazy val failOnStdErr: Boolean = RuntimeAttributesValidation.extract( + FailOnStderrValidation.instance, validatedRuntimeAttributes) /** * Returns the behavior for continuing on the return code, obtained by converting `returnCodeContents` to an Int. * * @return the behavior for continuing on the return code. */ - def continueOnReturnCode: ContinueOnReturnCode = ContinueOnReturnCodeFlag(false) - - /** - * Returns the metadata key values to store before executing a job. - * - * @return the metadata key values to store before executing a job. - */ - def startMetadataKeyValues: Map[String, Any] = Map.empty + lazy val continueOnReturnCode: ContinueOnReturnCode = RuntimeAttributesValidation.extract( + ContinueOnReturnCodeValidation.instance, validatedRuntimeAttributes) /** * Execute the job specified in the params. Should return a `StandardAsyncPendingExecutionHandle`, or a @@ -410,8 +373,8 @@ trait StandardAsyncExecutionActor extends AsyncBackendJobExecutionActor { try { if (isSuccess(status)) { - lazy val stderrLength: Long = File(remoteStdErrPath).size - lazy val returnCode: Try[Int] = Try(returnCodeContents).map(_.trim.toInt) + lazy val stderrLength: Long = File(jobPaths.stderr).size + lazy val returnCode: Try[Int] = Try(File(jobPaths.returnCode).contentAsString).map(_.trim.toInt) status match { case _ if failOnStdErr && stderrLength.intValue > 0 => // returnCode will be None if it couldn't be downloaded/parsed, which will yield a null in the DB diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardCacheHitCopyingActor.scala b/backend/src/main/scala/cromwell/backend/standard/StandardCacheHitCopyingActor.scala new file mode 100644 index 000000000..7f5531e94 --- /dev/null +++ b/backend/src/main/scala/cromwell/backend/standard/StandardCacheHitCopyingActor.scala @@ -0,0 +1,46 @@ +package cromwell.backend.standard + +import java.nio.file.Path + +import akka.actor.ActorRef +import cromwell.backend.callcaching.CacheHitDuplicating +import cromwell.backend.{BackendCacheHitCopyingActor, BackendConfigurationDescriptor, BackendInitializationData, BackendJobDescriptor} + +/** + * Trait of parameters passed to a StandardCacheHitCopyingActor. + */ +trait StandardCacheHitCopyingActorParams { + def jobDescriptor: BackendJobDescriptor + + def backendInitializationDataOption: Option[BackendInitializationData] + + def serviceRegistryActor: ActorRef + + def configurationDescriptor: BackendConfigurationDescriptor +} + +/** A default implementation of the cache hit copying params. */ +case class DefaultStandardCacheHitCopyingActorParams +( + override val jobDescriptor: BackendJobDescriptor, + override val backendInitializationDataOption: Option[BackendInitializationData], + override val serviceRegistryActor: ActorRef, + override val configurationDescriptor: BackendConfigurationDescriptor +) extends StandardCacheHitCopyingActorParams + +/** + * Standard implementation of a BackendCacheHitCopyingActor. + */ +trait StandardCacheHitCopyingActor extends BackendCacheHitCopyingActor with CacheHitDuplicating + with StandardCachingActorHelper { + + def standardParams: StandardCacheHitCopyingActorParams + + override lazy val jobDescriptor: BackendJobDescriptor = standardParams.jobDescriptor + override lazy val backendInitializationDataOption: Option[BackendInitializationData] = + standardParams.backendInitializationDataOption + override lazy val serviceRegistryActor: ActorRef = standardParams.serviceRegistryActor + override lazy val configurationDescriptor: BackendConfigurationDescriptor = standardParams.configurationDescriptor + override lazy val destinationCallRootPath: Path = jobPaths.callRoot + override lazy val destinationJobDetritusPaths: Map[String, Path] = jobPaths.detritusPaths +} diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardCachingActorHelper.scala b/backend/src/main/scala/cromwell/backend/standard/StandardCachingActorHelper.scala new file mode 100644 index 000000000..db93cc1b5 --- /dev/null +++ b/backend/src/main/scala/cromwell/backend/standard/StandardCachingActorHelper.scala @@ -0,0 +1,89 @@ +package cromwell.backend.standard + +import java.nio.file.Path + +import akka.actor.{Actor, ActorRef} +import cromwell.backend._ +import cromwell.backend.callcaching.JobCachingActorHelper +import cromwell.backend.io.{JobPaths, WorkflowPaths} +import cromwell.backend.validation.{RuntimeAttributesValidation, ValidatedRuntimeAttributes} +import cromwell.core.logging.JobLogging +import wdl4s.TaskCall + +import scala.util.Try + +/** + * Extends the JobCachingActorHelper with standard implementations. + * + * Like the JobCachingActorHelper, this trait should be extended by a backend, and then __that__ trait should be mixed + * into a async job execution actor, and cache hit copying actor. + */ +trait StandardCachingActorHelper extends JobCachingActorHelper { + this: Actor with JobLogging => + + def backendInitializationDataOption: Option[BackendInitializationData] + + /** Typed backend initialization. */ + def backendInitializationDataAs[A <: BackendInitializationData]: A = + BackendInitializationData.as[A](backendInitializationDataOption) + + /** + * Returns the service registry actor. Both the StandardAsyncExecutorActor and StandardCacheHitCopyingActor traits + * implement this method. + * + * @return Paths to the job. + */ + def serviceRegistryActor: ActorRef + + // So... JobPaths doesn't extend WorkflowPaths, but does contain a self-type + lazy val workflowPaths: WorkflowPaths = jobPaths.asInstanceOf[WorkflowPaths] + + def getPath(str: String): Try[Path] = workflowPaths.getPath(str) + + /** + * The workflow descriptor for this job. NOTE: This may be different than the workflow descriptor created in the + * workflow initialization data. For example, sub workflows use a different workflow descriptor. + */ + lazy val workflowDescriptor: BackendWorkflowDescriptor = jobDescriptor.workflowDescriptor + + lazy val call: TaskCall = jobDescriptor.key.call + + lazy val standardInitializationData: StandardInitializationData = BackendInitializationData. + as[StandardInitializationData](backendInitializationDataOption) + + lazy val validatedRuntimeAttributes: ValidatedRuntimeAttributes = { + val builder = standardInitializationData.runtimeAttributesBuilder + builder.build(jobDescriptor.runtimeAttributes, jobLogger) + } + + /** + * Returns the paths to the job. + * + * @return Paths to the job. + */ + lazy val jobPaths: JobPaths = standardInitializationData.workflowPaths.toJobPaths(jobDescriptor) + + /** + * Returns the metadata key values to store before executing a job. + * + * @return the metadata key values to store before executing a job. + */ + def startMetadataKeyValues: Map[String, Any] = { + val runtimeAttributesMetadata = RuntimeAttributesValidation.toMetadataStrings(validatedRuntimeAttributes) map { + case (key, value) => (s"runtimeAttributes:$key", value) + } + + val fileMetadata = jobPaths.metadataPaths + + val otherMetadata = Map("cache:allowResultReuse" -> true) + + runtimeAttributesMetadata ++ fileMetadata ++ otherMetadata ++ nonStandardMetadata + } + + /** + * Returns any custom medatata for the backend. + * + * @return any custom metadata for the backend. + */ + protected def nonStandardMetadata: Map[String, Any] = Map.empty +} diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardFinalizationActor.scala b/backend/src/main/scala/cromwell/backend/standard/StandardFinalizationActor.scala new file mode 100644 index 000000000..19500a90d --- /dev/null +++ b/backend/src/main/scala/cromwell/backend/standard/StandardFinalizationActor.scala @@ -0,0 +1,41 @@ +package cromwell.backend.standard + +import cromwell.backend._ +import cromwell.core.CallOutputs +import wdl4s.TaskCall + +trait StandardFinalizationActorParams { + def workflowDescriptor: BackendWorkflowDescriptor + + def calls: Set[TaskCall] + + def jobExecutionMap: JobExecutionMap + + def workflowOutputs: CallOutputs + + def initializationDataOption: Option[BackendInitializationData] + + def configurationDescriptor: BackendConfigurationDescriptor +} + +case class DefaultStandardFinalizationActorParams +( + workflowDescriptor: BackendWorkflowDescriptor, + calls: Set[TaskCall], + jobExecutionMap: JobExecutionMap, + workflowOutputs: CallOutputs, + initializationDataOption: Option[BackendInitializationData], + configurationDescriptor: BackendConfigurationDescriptor +) extends StandardFinalizationActorParams + +trait StandardFinalizationActor extends BackendWorkflowFinalizationActor { + + def standardParams: StandardFinalizationActorParams + + override lazy val workflowDescriptor: BackendWorkflowDescriptor = standardParams.workflowDescriptor + override lazy val calls: Set[TaskCall] = standardParams.calls + lazy val initializationDataOption: Option[BackendInitializationData] = standardParams.initializationDataOption + lazy val jobExecutionMap: JobExecutionMap = standardParams.jobExecutionMap + lazy val workflowOutputs: CallOutputs = standardParams.workflowOutputs + override lazy val configurationDescriptor: BackendConfigurationDescriptor = standardParams.configurationDescriptor +} diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardInitializationActor.scala b/backend/src/main/scala/cromwell/backend/standard/StandardInitializationActor.scala new file mode 100644 index 000000000..1fbfc92d1 --- /dev/null +++ b/backend/src/main/scala/cromwell/backend/standard/StandardInitializationActor.scala @@ -0,0 +1,70 @@ +package cromwell.backend.standard + +import akka.actor.ActorRef +import cromwell.backend.validation.RuntimeAttributesDefault +import cromwell.backend.{BackendConfigurationDescriptor, BackendWorkflowDescriptor, BackendWorkflowInitializationActor} +import cromwell.core.WorkflowOptions +import wdl4s.TaskCall +import wdl4s.values.WdlValue + +import scala.concurrent.Future +import scala.util.Try + +trait StandardInitializationActorParams { + def workflowDescriptor: BackendWorkflowDescriptor + + def calls: Set[TaskCall] + + def serviceRegistryActor: ActorRef + + def configurationDescriptor: BackendConfigurationDescriptor +} + +case class DefaultInitializationActorParams +( + workflowDescriptor: BackendWorkflowDescriptor, + calls: Set[TaskCall], + serviceRegistryActor: ActorRef, + configurationDescriptor: BackendConfigurationDescriptor +) extends StandardInitializationActorParams + +trait StandardInitializationActor extends BackendWorkflowInitializationActor { + + def standardParams: StandardInitializationActorParams + + override lazy val serviceRegistryActor: ActorRef = standardParams.serviceRegistryActor + + override lazy val calls: Set[TaskCall] = standardParams.calls + + def runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder = + StandardValidatedRuntimeAttributesBuilder.default + + override protected def runtimeAttributeValidators: Map[String, (Option[WdlValue]) => Boolean] = { + runtimeAttributesBuilder.validatorMap + } + + override protected def coerceDefaultRuntimeAttributes(options: WorkflowOptions): Try[Map[String, WdlValue]] = { + RuntimeAttributesDefault.workflowOptionsDefault(options, runtimeAttributesBuilder.coercionMap) + } + + override def validate(): Future[Unit] = { + Future.fromTry(Try { + calls foreach { call => + val runtimeAttributeKeys = call.task.runtimeAttributes.attrs.keys.toList + val notSupportedAttributes = runtimeAttributesBuilder.unsupportedKeys(runtimeAttributeKeys).toList + + if (notSupportedAttributes.nonEmpty) { + val notSupportedAttrString = notSupportedAttributes mkString ", " + workflowLogger.warn( + s"Key/s [$notSupportedAttrString] is/are not supported by backend. " + + s"Unsupported attributes will not be part of job executions.") + } + } + }) + } + + override protected def workflowDescriptor: BackendWorkflowDescriptor = standardParams.workflowDescriptor + + override protected def configurationDescriptor: BackendConfigurationDescriptor = + standardParams.configurationDescriptor +} diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardInitializationData.scala b/backend/src/main/scala/cromwell/backend/standard/StandardInitializationData.scala new file mode 100644 index 000000000..e9cdb2f35 --- /dev/null +++ b/backend/src/main/scala/cromwell/backend/standard/StandardInitializationData.scala @@ -0,0 +1,21 @@ +package cromwell.backend.standard + +import cromwell.backend.BackendInitializationData +import cromwell.backend.io.WorkflowPaths +import cromwell.core.path.PathBuilder + +class StandardInitializationData +( + val workflowPaths: WorkflowPaths, + val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder +) extends BackendInitializationData + +object StandardInitializationData { + def workflowPaths(initializationDataOption: Option[BackendInitializationData]): WorkflowPaths = { + BackendInitializationData.as[StandardInitializationData](initializationDataOption).workflowPaths + } + + def pathBuilders(initializationDataOption: Option[BackendInitializationData]): List[PathBuilder] = { + workflowPaths(initializationDataOption).pathBuilders + } +} diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardJobExecutionActorParams.scala b/backend/src/main/scala/cromwell/backend/standard/StandardJobExecutionActorParams.scala index 9242aa601..1261119d9 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardJobExecutionActorParams.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardJobExecutionActorParams.scala @@ -12,19 +12,22 @@ import scala.language.existentials */ trait StandardJobExecutionActorParams { /** The service registry actor for key/value and metadata. */ - val serviceRegistryActor: ActorRef + def serviceRegistryActor: ActorRef /** The descriptor of this job. */ - val jobDescriptor: BackendJobDescriptor + def jobDescriptor: BackendJobDescriptor /** The global and backend configuration. */ - val configurationDescriptor: BackendConfigurationDescriptor + def configurationDescriptor: BackendConfigurationDescriptor /** Any backend initialization data. */ - val backendInitializationDataOption: Option[BackendInitializationData] + def backendInitializationDataOption: Option[BackendInitializationData] /** The key for this job. */ - val jobIdKey: String + def jobIdKey: String + + /** The singleton actor. */ + def backendSingletonActorOption: Option[ActorRef] } /** @@ -36,7 +39,7 @@ trait StandardSyncExecutionActorParams extends StandardJobExecutionActorParams { * * @see [[StandardSyncExecutionActor]] */ - val asyncJobExecutionActorClass: Class[_ <: StandardAsyncExecutionActor] + def asyncJobExecutionActorClass: Class[_ <: StandardAsyncExecutionActor] } /** A default implementation of the sync params. */ @@ -47,6 +50,7 @@ case class DefaultStandardSyncExecutionActorParams override val jobDescriptor: BackendJobDescriptor, override val configurationDescriptor: BackendConfigurationDescriptor, override val backendInitializationDataOption: Option[BackendInitializationData], + override val backendSingletonActorOption: Option[ActorRef], override val asyncJobExecutionActorClass: Class[_ <: StandardAsyncExecutionActor] ) extends StandardSyncExecutionActorParams @@ -55,7 +59,7 @@ case class DefaultStandardSyncExecutionActorParams */ trait StandardAsyncExecutionActorParams extends StandardJobExecutionActorParams { /** The promise that will be completed when the async run is complete. */ - val completionPromise: Promise[BackendJobExecutionResponse] + def completionPromise: Promise[BackendJobExecutionResponse] } /** A default implementation of the async params. */ @@ -66,5 +70,6 @@ case class DefaultStandardAsyncExecutionActorParams override val jobDescriptor: BackendJobDescriptor, override val configurationDescriptor: BackendConfigurationDescriptor, override val backendInitializationDataOption: Option[BackendInitializationData], + override val backendSingletonActorOption: Option[ActorRef], override val completionPromise: Promise[BackendJobExecutionResponse] ) extends StandardAsyncExecutionActorParams diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala b/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala index 6a0f9324b..aa7e57356 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala @@ -1,8 +1,13 @@ package cromwell.backend.standard +import java.nio.file.Path + import akka.actor.{ActorRef, Props} +import com.typesafe.config.Config import cromwell.backend._ -import cromwell.core.Dispatcher +import cromwell.core.{CallOutputs, Dispatcher} +import cromwell.core.Dispatcher.BackendDispatcher +import wdl4s.TaskCall /** * May be extended for using the standard sync/async backend pattern. @@ -18,11 +23,40 @@ trait StandardLifecycleActorFactory extends BackendLifecycleActorFactory { def configurationDescriptor: BackendConfigurationDescriptor /** - * Returns the main engine for async execution. + * Returns the initialization class. + * + * @return the initialization class. + */ + def initializationActorClass: Class[_ <: StandardInitializationActor] + + /** + * Returns the synchronous executor class. By default using the standard sync executor should be sufficient for most + * implementations. * - * @return the main engine for async execution. + * @return the synchronous executor class. */ - def asyncJobExecutionActorClass: Class[_ <: StandardAsyncExecutionActor] + def syncExecutionActorClass: Class[_ <: StandardSyncExecutionActor] = classOf[StandardSyncExecutionActor] + + /** + * Returns the asynchronous executor class. + * + * @return the asynchronous executor class. + */ + def asyncExecutionActorClass: Class[_ <: StandardAsyncExecutionActor] + + /** + * Returns the cache hit copying class. + * + * @return the cache hit copying class. + */ + def standardCacheHitCopyingActorOption: Option[Class[_ <: StandardCacheHitCopyingActor]] = None + + /** + * Returns the finalization class. + * + * @return the finalization class. + */ + def finalizationActorClassOption: Option[Class[_ <: StandardFinalizationActor]] = None /** * Returns the key to use for storing and looking up the job id. @@ -31,12 +65,83 @@ trait StandardLifecycleActorFactory extends BackendLifecycleActorFactory { */ def jobIdKey: String + override def workflowInitializationActorProps(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], + serviceRegistryActor: ActorRef): Option[Props] = { + val params = workflowInitializationActorParams(workflowDescriptor, calls, serviceRegistryActor) + val props = Props(initializationActorClass, params).withDispatcher(Dispatcher.BackendDispatcher) + Option(props) + } + + def workflowInitializationActorParams(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], + serviceRegistryActor: ActorRef): StandardInitializationActorParams = { + DefaultInitializationActorParams(workflowDescriptor, calls, serviceRegistryActor, configurationDescriptor) + } + override def jobExecutionActorProps(jobDescriptor: BackendJobDescriptor, initializationDataOption: Option[BackendInitializationData], serviceRegistryActor: ActorRef, - backendSingletonActor: Option[ActorRef]): Props = { - val params = DefaultStandardSyncExecutionActorParams(jobIdKey, serviceRegistryActor, - jobDescriptor, configurationDescriptor, initializationDataOption, asyncJobExecutionActorClass) + backendSingletonActorOption: Option[ActorRef]): Props = { + val params = jobExecutionActorParams(jobDescriptor, initializationDataOption, serviceRegistryActor, + backendSingletonActorOption) Props(new StandardSyncExecutionActor(params)).withDispatcher(Dispatcher.BackendDispatcher) } + + def jobExecutionActorParams(jobDescriptor: BackendJobDescriptor, + initializationDataOption: Option[BackendInitializationData], + serviceRegistryActor: ActorRef, + backendSingletonActorOption: Option[ActorRef]): StandardSyncExecutionActorParams = { + DefaultStandardSyncExecutionActorParams(jobIdKey, serviceRegistryActor, jobDescriptor, configurationDescriptor, + initializationDataOption, backendSingletonActorOption, asyncExecutionActorClass) + } + + override def cacheHitCopyingActorProps: + Option[(BackendJobDescriptor, Option[BackendInitializationData], ActorRef) => Props] = { + standardCacheHitCopyingActorOption map { + standardCacheHitCopyingActor => cacheHitCopyingActorInner(standardCacheHitCopyingActor) _ + } + } + + def cacheHitCopyingActorInner(standardCacheHitCopyingActor: Class[_ <: StandardCacheHitCopyingActor]) + (jobDescriptor: BackendJobDescriptor, + initializationDataOption: Option[BackendInitializationData], + serviceRegistryActor: ActorRef): Props = { + val params = cacheHitCopyingActorParams(jobDescriptor, initializationDataOption, serviceRegistryActor) + Props(standardCacheHitCopyingActor, params).withDispatcher(BackendDispatcher) + } + + def cacheHitCopyingActorParams(jobDescriptor: BackendJobDescriptor, + initializationDataOption: Option[BackendInitializationData], + serviceRegistryActor: ActorRef): StandardCacheHitCopyingActorParams = { + DefaultStandardCacheHitCopyingActorParams( + jobDescriptor, initializationDataOption, serviceRegistryActor, configurationDescriptor) + } + + override def workflowFinalizationActorProps(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], + jobExecutionMap: JobExecutionMap, workflowOutputs: CallOutputs, + initializationData: Option[BackendInitializationData]): Option[Props] = { + finalizationActorClassOption map { finalizationActorClass => + val params = workflowFinalizationActorParams(workflowDescriptor, calls, jobExecutionMap, workflowOutputs, + initializationData) + Props(finalizationActorClass, params).withDispatcher(BackendDispatcher) + } + } + + def workflowFinalizationActorParams(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], + jobExecutionMap: JobExecutionMap, workflowOutputs: CallOutputs, + initializationDataOption: Option[BackendInitializationData]): + StandardFinalizationActorParams = { + DefaultStandardFinalizationActorParams(workflowDescriptor, calls, jobExecutionMap, workflowOutputs, + initializationDataOption, configurationDescriptor) + } + + override def getExecutionRootPath(workflowDescriptor: BackendWorkflowDescriptor, backendConfig: Config, + initializationData: Option[BackendInitializationData]): Path = { + initializationData.get.asInstanceOf[StandardInitializationData].workflowPaths.executionRoot + } + + override def getWorkflowExecutionRootPath(workflowDescriptor: BackendWorkflowDescriptor, backendConfig: Config, + initializationData: Option[BackendInitializationData]): Path = { + initializationData.get.asInstanceOf[StandardInitializationData].workflowPaths.workflowRoot + } + } diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardSyncExecutionActor.scala b/backend/src/main/scala/cromwell/backend/standard/StandardSyncExecutionActor.scala index 52bba0153..472fffa0f 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardSyncExecutionActor.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardSyncExecutionActor.scala @@ -97,6 +97,7 @@ class StandardSyncExecutionActor(val standardParams: StandardSyncExecutionActorP standardParams.jobDescriptor, standardParams.configurationDescriptor, standardParams.backendInitializationDataOption, + standardParams.backendSingletonActorOption, completionPromise ) } diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilder.scala b/backend/src/main/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilder.scala index 55be9e663..20a649e00 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilder.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilder.scala @@ -1,105 +1,50 @@ -package cromwell.backend.sfs +package cromwell.backend.standard import cromwell.backend.validation._ /** - * Validates a collection of runtime attributes for a Shared File System (SFS) Backend. + * Validates a collection of runtime attributes for a Standard Backend. * - * There are always three collections of runtime attributes, two of which are required for the SFS base class: + * There are two collections of runtime attributes, the first that are required for the standard async execution, and + * custom validations that the backend sub class may specify. * - * Required for the SFS to be able to run properly: - * 1) The set of runtime attributes that are absolutely required, or the validation fails. - * 2) _Extra_ validations that will run for the SFS. + * Currently the required validations are: + * - `ContinueOnReturnCodeValidation.default` + * - `FailOnStderrValidation.default` * - * Lastly for the sub classes of the SFS, there are: - * 3) Custom validations that the backend sub class may specify. - * - * 3), the custom validations, are always set by the sub class, via calls to `withValidation()`. - * - * For 1) and 2) the biggest difference is when it comes to docker support in a backend. - * - * For a backend that does __not__ support docker, the default, 1) and 2) will contain: - * - * 1) required = ContinueOnReturnCodeValidation.default, FailOnStderrValidation.default - * 2) unsupportedExtra = DockerValidation.optional - * - * Suppose the above validation runs a WDL with runtime attributes shows up with: - * - * {{{ - * runtimeAttributes { - * continueOnReturnCode: 14 - * # failOnStdErr not specified - * docker: "ubuntu" - * } - * }}} - * - * This will cause a validation warning to print out that docker is unsupported, because the docker validation isn't - * present in the required validations. If/when the SFS backend asks what the value of the optional docker is, it will - * receive `None`, as the validation _is not_ listed in the required validations. - * - * There is an additional interesting thing about having the docker validation still running under an "unsupported - * extra validation". Say an invalid docker were to be specified as a WdlArray. The extra validation __would__ catch - * the error, even after the warning had been printed stating that docker is not supported by the backend. - * - * Meanwhile, even though failOnStdErr is not specified, the `FailOnStderrValidation.default` will return its default - * value. And of course, the `ContinueOnReturnCodeValidation.default` returns the specified, and valid, runtime - * attribute `ContinueOnReturnCodeSet(14)`. - * - * Now-- - * - * Suppose the `withDockerSupport(true)` has been invoked on the builder. The required and unsupported runtime - * attributes will then look like: - * - * 1) required = ContinueOnReturnCodeValidation.default, FailOnStderrValidation.default, - * DockerValidation.optional - * 2) unsupportedExtra = __empty__ - * - * With the same WDL above, this builder does NOT print a warning, because docker __is__ supported. When the SFS asks - * for the optional docker element, it receives `Some("ubuntu")`, as the `DockerValidation.optional` __is__ listed in - * the required validations. - * - * `ContinueOnReturnCodeValidation.default` and `FailOnStderrValidation.default` still operate as in the previous - * example. - * - * What happens when there is no runtime attribute for docker? Easy, the validation for docker is always optional! - * In either case of running a builder via `withDockerSupport(true)` or `withDockerSupport(false)`, if the docker - * runtime attribute is not specified, the `SharedFileSystemValidatedRuntimeAttributesBuilder` will return a - * `None` value. + * NOTE: The required runtime attributes may be moved to the engine in the future. */ -object SharedFileSystemValidatedRuntimeAttributesBuilder { +object StandardValidatedRuntimeAttributesBuilder { - private case class SharedFileSystemValidatedRuntimeAttributesBuilderImpl + private case class StandardValidatedRuntimeAttributesBuilderImpl ( override val requiredValidations: Seq[RuntimeAttributesValidation[_]], override val customValidations: Seq[RuntimeAttributesValidation[_]] - ) extends SharedFileSystemValidatedRuntimeAttributesBuilder + ) extends StandardValidatedRuntimeAttributesBuilder /** - * `default` returns the default set of attributes required to run an SFS: + * `default` returns the default set of attributes required to run a standard backend: * - `ContinueOnReturnCodeValidation.default` * - `FailOnStderrValidation.default` * * Additional runtime attribute validations may be added by calling `withValidation` on the default. - * - * The SFS will also _always_ validate using the `DockerValidation`, but will end up warning the user that the - * runtime attribute is unsupported by the backend implementation unless `withDockerSupport(true)` is called. */ - lazy val default: SharedFileSystemValidatedRuntimeAttributesBuilder = { + lazy val default: StandardValidatedRuntimeAttributesBuilder = { val required = Seq(ContinueOnReturnCodeValidation.default, FailOnStderrValidation.default) val custom = Seq.empty - SharedFileSystemValidatedRuntimeAttributesBuilderImpl(custom, required) + StandardValidatedRuntimeAttributesBuilderImpl(custom, required) } - private def withValidations(builder: SharedFileSystemValidatedRuntimeAttributesBuilder, + private def withValidations(builder: StandardValidatedRuntimeAttributesBuilder, customValidations: Seq[RuntimeAttributesValidation[_]]): - SharedFileSystemValidatedRuntimeAttributesBuilder = { + StandardValidatedRuntimeAttributesBuilder = { val required = builder.requiredValidations val custom = builder.customValidations ++ customValidations - SharedFileSystemValidatedRuntimeAttributesBuilderImpl(custom, required) + StandardValidatedRuntimeAttributesBuilderImpl(custom, required) } } -sealed trait SharedFileSystemValidatedRuntimeAttributesBuilder extends ValidatedRuntimeAttributesBuilder { +sealed trait StandardValidatedRuntimeAttributesBuilder extends ValidatedRuntimeAttributesBuilder { /** * Returns a new builder with the additional validation(s). * @@ -107,14 +52,14 @@ sealed trait SharedFileSystemValidatedRuntimeAttributesBuilder extends Validated * @return New builder with the validation. */ final def withValidation(validation: RuntimeAttributesValidation[_]*): - SharedFileSystemValidatedRuntimeAttributesBuilder = { - SharedFileSystemValidatedRuntimeAttributesBuilder.withValidations(this, validation) + StandardValidatedRuntimeAttributesBuilder = { + StandardValidatedRuntimeAttributesBuilder.withValidations(this, validation) } - /** Returns on the supported validations, those required for the SFS, plus custom addons for the subclass. */ + /** Returns all the validations, those required for the standard backend, plus custom addons for the subclass. */ override final lazy val validations: Seq[RuntimeAttributesValidation[_]] = requiredValidations ++ customValidations - private[sfs] def requiredValidations: Seq[RuntimeAttributesValidation[_]] + private[standard] def requiredValidations: Seq[RuntimeAttributesValidation[_]] - private[sfs] def customValidations: Seq[RuntimeAttributesValidation[_]] + private[standard] def customValidations: Seq[RuntimeAttributesValidation[_]] } diff --git a/backend/src/main/scala/cromwell/backend/validation/ContinueOnReturnCodeValidation.scala b/backend/src/main/scala/cromwell/backend/validation/ContinueOnReturnCodeValidation.scala index db0cacaf0..d0ca49339 100644 --- a/backend/src/main/scala/cromwell/backend/validation/ContinueOnReturnCodeValidation.scala +++ b/backend/src/main/scala/cromwell/backend/validation/ContinueOnReturnCodeValidation.scala @@ -6,8 +6,8 @@ import cats.syntax.traverse._ import cats.syntax.validated._ import cromwell.backend.validation.RuntimeAttributesValidation._ import lenthall.validation.ErrorOr._ -import wdl4s.types.{WdlArrayType, WdlIntegerType, WdlStringType} -import wdl4s.values.{WdlArray, WdlBoolean, WdlInteger, WdlString} +import wdl4s.types.{WdlArrayType, WdlIntegerType, WdlStringType, WdlType} +import wdl4s.values.{WdlArray, WdlBoolean, WdlInteger, WdlString, WdlValue} import scala.util.Try @@ -23,45 +23,44 @@ import scala.util.Try * found. */ object ContinueOnReturnCodeValidation { - val key = RuntimeAttributesKeys.ContinueOnReturnCodeKey - - lazy val instance = new ContinueOnReturnCodeValidation - - lazy val default = instance.withDefault(WdlInteger(0)) - - lazy val optional = default.optional - - private[validation] val missingMessage = - s"Expecting $key runtime attribute to be either a Boolean, a String 'true' or 'false', or an Array[Int]" + lazy val instance: RuntimeAttributesValidation[ContinueOnReturnCode] = new ContinueOnReturnCodeValidation + lazy val default: RuntimeAttributesValidation[ContinueOnReturnCode] = instance.withDefault(WdlInteger(0)) + lazy val optional: OptionalRuntimeAttributesValidation[ContinueOnReturnCode] = default.optional } class ContinueOnReturnCodeValidation extends RuntimeAttributesValidation[ContinueOnReturnCode] { - import ContinueOnReturnCodeValidation._ + override def key: String = RuntimeAttributesKeys.ContinueOnReturnCodeKey - override def key = RuntimeAttributesKeys.ContinueOnReturnCodeKey + override def coercion: Set[WdlType] = ContinueOnReturnCode.validWdlTypes - override def coercion = ContinueOnReturnCode.validWdlTypes - - override def validateValue = { + override def validateValue: PartialFunction[WdlValue, ErrorOr[ContinueOnReturnCode]] = { case WdlBoolean(value) => ContinueOnReturnCodeFlag(value).validNel case WdlString(value) if Try(value.toBoolean).isSuccess => ContinueOnReturnCodeFlag(value.toBoolean).validNel + case WdlString(value) if Try(value.toInt).isSuccess => ContinueOnReturnCodeSet(Set(value.toInt)).validNel case WdlInteger(value) => ContinueOnReturnCodeSet(Set(value)).validNel - case WdlArray(wdlType, seq) => + case value@WdlArray(_, seq) => val errorOrInts: ErrorOr[List[Int]] = (seq.toList map validateInt).sequence[ErrorOr, Int] errorOrInts match { case Valid(ints) => ContinueOnReturnCodeSet(ints.toSet).validNel - case Invalid(_) => failureWithMessage + case Invalid(_) => invalidValueFailure(value) } } - override def validateExpression = { - case _: WdlBoolean => true + override def validateExpression: PartialFunction[WdlValue, Boolean] = { + case WdlBoolean(_) => true + case WdlString(value) if Try(value.toInt).isSuccess => true case WdlString(value) if Try(value.toBoolean).isSuccess => true - case _: WdlInteger => true - case WdlArray(WdlArrayType(WdlStringType), elements) => elements.forall(validateInt(_).isValid) - case WdlArray(WdlArrayType(WdlIntegerType), elements) => elements.forall(validateInt(_).isValid) + case WdlInteger(_) => true + case WdlArray(WdlArrayType(WdlStringType), elements) => + elements forall { + value => Try(value.valueString.toInt).isSuccess + } + case WdlArray(WdlArrayType(WdlIntegerType), _) => true } - override protected def failureMessage = missingMessage + override protected def missingValueMessage: String = s"Expecting $key" + + " runtime attribute to be either a Boolean, a String 'true' or 'false', or an Array[Int]" + + override protected def usedInCallCaching: Boolean = true } diff --git a/backend/src/main/scala/cromwell/backend/validation/CpuValidation.scala b/backend/src/main/scala/cromwell/backend/validation/CpuValidation.scala index 43303b6cb..5a5d09730 100644 --- a/backend/src/main/scala/cromwell/backend/validation/CpuValidation.scala +++ b/backend/src/main/scala/cromwell/backend/validation/CpuValidation.scala @@ -1,8 +1,9 @@ package cromwell.backend.validation import cats.syntax.validated._ +import lenthall.validation.ErrorOr.ErrorOr import wdl4s.types.WdlIntegerType -import wdl4s.values.WdlInteger +import wdl4s.values.{WdlInteger, WdlValue} /** * Validates the "cpu" runtime attribute an Integer greater than 0, returning the value as an `Int`. @@ -15,36 +16,22 @@ import wdl4s.values.WdlInteger * found. */ object CpuValidation extends { - val key = RuntimeAttributesKeys.CpuKey - - lazy val instance = new CpuValidation - - lazy val default = instance.withDefault(WdlInteger(1)) - - lazy val optional = default.optional - - private[validation] val missingMessage = s"Expecting $key runtime attribute to be an Integer" - private[validation] val wrongAmountMsg = s"Expecting $key runtime attribute value greater than 0" + lazy val instance: RuntimeAttributesValidation[Int] = new CpuValidation + lazy val default: RuntimeAttributesValidation[Int] = instance.withDefault(WdlInteger(1)) + lazy val optional: OptionalRuntimeAttributesValidation[Int] = default.optional } -class CpuValidation extends RuntimeAttributesValidation[Int] { - - import CpuValidation._ - - override def key = RuntimeAttributesKeys.CpuKey - - override def coercion = Seq(WdlIntegerType) - - override protected def validateValue = { +class CpuValidation extends IntRuntimeAttributesValidation(RuntimeAttributesKeys.CpuKey) { + override protected def validateValue: PartialFunction[WdlValue, ErrorOr[Int]] = { case wdlValue if WdlIntegerType.coerceRawValue(wdlValue).isSuccess => WdlIntegerType.coerceRawValue(wdlValue).get match { - case WdlInteger(value) => if (value.toInt <= 0) wrongAmountMsg.invalidNel else value.toInt.validNel + case WdlInteger(value) => + if (value.toInt <= 0) + s"Expecting $key runtime attribute value greater than 0".invalidNel + else + value.toInt.validNel } } - override def validateExpression = { - case wdlValue if WdlIntegerType.coerceRawValue(wdlValue).isSuccess => true - } - - override protected def failureMessage = missingMessage + override protected def missingValueMessage: String = s"Expecting $key runtime attribute to be an Integer" } diff --git a/backend/src/main/scala/cromwell/backend/validation/DockerValidation.scala b/backend/src/main/scala/cromwell/backend/validation/DockerValidation.scala index 12e090fcf..f793d1397 100644 --- a/backend/src/main/scala/cromwell/backend/validation/DockerValidation.scala +++ b/backend/src/main/scala/cromwell/backend/validation/DockerValidation.scala @@ -1,8 +1,8 @@ package cromwell.backend.validation import cats.syntax.validated._ -import wdl4s.types.WdlStringType -import wdl4s.values.WdlString +import lenthall.validation.ErrorOr.ErrorOr +import wdl4s.values.{WdlString, WdlValue} /** * Validates the "docker" runtime attribute as a String, returning it as `String`. @@ -13,26 +13,19 @@ import wdl4s.values.WdlString * if present, or `None` if not found. */ object DockerValidation { - val key = RuntimeAttributesKeys.DockerKey - - lazy val instance = new DockerValidation - - lazy val optional = instance.optional - - private[validation] val missingMessage = s"Expecting $key runtime attribute to be a String" + lazy val instance: RuntimeAttributesValidation[String] = new DockerValidation + lazy val optional: OptionalRuntimeAttributesValidation[String] = instance.optional } -class DockerValidation extends RuntimeAttributesValidation[String] { +class DockerValidation extends StringRuntimeAttributesValidation(RuntimeAttributesKeys.DockerKey) { + override protected def usedInCallCaching: Boolean = true - import DockerValidation._ + override protected def missingValueMessage: String = "Can't find an attribute value for key docker" - override def key = RuntimeAttributesKeys.DockerKey + override protected def invalidValueMessage(value: WdlValue): String = super.missingValueMessage - override def coercion = Seq(WdlStringType) - - override protected def validateValue = { + // NOTE: Docker's current test specs don't like WdlInteger, etc. auto converted to WdlString. + override protected def validateValue: PartialFunction[WdlValue, ErrorOr[String]] = { case WdlString(value) => value.validNel } - - override protected def failureMessage = missingMessage } diff --git a/backend/src/main/scala/cromwell/backend/validation/FailOnStderrValidation.scala b/backend/src/main/scala/cromwell/backend/validation/FailOnStderrValidation.scala index 6c75a40cf..6a7ea6ee2 100644 --- a/backend/src/main/scala/cromwell/backend/validation/FailOnStderrValidation.scala +++ b/backend/src/main/scala/cromwell/backend/validation/FailOnStderrValidation.scala @@ -1,10 +1,6 @@ package cromwell.backend.validation -import cats.syntax.validated._ -import wdl4s.types.{WdlBooleanType, WdlStringType} -import wdl4s.values.{WdlBoolean, WdlString} - -import scala.util.Try +import wdl4s.values.WdlBoolean /** * Validates the "failOnStderr" runtime attribute as a Boolean or a String 'true' or 'false', returning the value as a @@ -15,35 +11,14 @@ import scala.util.Try * The default returns `false` when no attribute is specified. */ object FailOnStderrValidation { - val key = RuntimeAttributesKeys.FailOnStderrKey - - lazy val instance = new FailOnStderrValidation - - lazy val default = instance.withDefault(WdlBoolean(false)) - - lazy val optional = default.optional - - private[validation] val missingMessage = - s"Expecting $key runtime attribute to be a Boolean or a String with values of 'true' or 'false'" + lazy val instance: RuntimeAttributesValidation[Boolean] = new FailOnStderrValidation + lazy val default: RuntimeAttributesValidation[Boolean] = instance.withDefault(WdlBoolean(false)) + lazy val optional: OptionalRuntimeAttributesValidation[Boolean] = default.optional } -class FailOnStderrValidation extends RuntimeAttributesValidation[Boolean] { +class FailOnStderrValidation extends BooleanRuntimeAttributesValidation(RuntimeAttributesKeys.FailOnStderrKey) { + override protected def usedInCallCaching: Boolean = true - import FailOnStderrValidation._ - - override def key = RuntimeAttributesKeys.FailOnStderrKey - - override def coercion = Seq(WdlBooleanType, WdlStringType) - - override protected def validateValue = { - case WdlBoolean(value) => value.validNel - case WdlString(value) if Try(value.toBoolean).isSuccess => value.toBoolean.validNel - } - - override def validateExpression = { - case _: WdlBoolean => true - case WdlString(value) if Try(value.toBoolean).isSuccess => true - } - - override protected def failureMessage = missingMessage + override protected def missingValueMessage: String = + s"Expecting $key runtime attribute to be a Boolean or a String with values of 'true' or 'false'" } diff --git a/backend/src/main/scala/cromwell/backend/validation/MemoryValidation.scala b/backend/src/main/scala/cromwell/backend/validation/MemoryValidation.scala index 1e1c807cf..d493e357e 100644 --- a/backend/src/main/scala/cromwell/backend/validation/MemoryValidation.scala +++ b/backend/src/main/scala/cromwell/backend/validation/MemoryValidation.scala @@ -5,7 +5,7 @@ import cromwell.backend.MemorySize import lenthall.validation.ErrorOr._ import wdl4s.parser.MemoryUnit import wdl4s.types.{WdlIntegerType, WdlStringType} -import wdl4s.values.{WdlInteger, WdlString} +import wdl4s.values.{WdlInteger, WdlString, WdlValue} /** * Validates the "memory" runtime attribute as an Integer or String with format '8 GB', returning the value as a @@ -19,18 +19,17 @@ import wdl4s.values.{WdlInteger, WdlString} * `withDefaultMemory` can be used to create a memory validation that defaults to a particular memory size. */ object MemoryValidation { - val key = RuntimeAttributesKeys.MemoryKey + lazy val instance: RuntimeAttributesValidation[MemorySize] = new MemoryValidation + lazy val optional: OptionalRuntimeAttributesValidation[MemorySize] = instance.optional - lazy val instance = new MemoryValidation + def withDefaultMemory(memorySize: MemorySize): RuntimeAttributesValidation[MemorySize] = + instance.withDefault(WdlInteger(memorySize.bytes.toInt)) - lazy val optional = instance.optional - - def withDefaultMemory(memorySize: MemorySize) = instance.withDefault(WdlInteger(memorySize.bytes.toInt)) - - private[validation] val missingMessage = - s"Expecting $key runtime attribute to be an Integer or String with format '8 GB'" - private[validation] val wrongAmountFormat = s"Expecting $key runtime attribute value greater than 0 but got %s" - private[validation] val missingFormat = s"$missingMessage. Exception: %s" + private[validation] val wrongAmountFormat = + s"Expecting ${RuntimeAttributesKeys.MemoryKey} runtime attribute value greater than 0 but got %s" + private[validation] val wrongTypeFormat = + s"Expecting ${RuntimeAttributesKeys.MemoryKey} runtime attribute to be an Integer or String with format '8 GB'." + + s" Exception: %s" private[validation] def validateMemoryString(wdlString: WdlString): ErrorOr[MemorySize] = validateMemoryString(wdlString.value) @@ -42,7 +41,7 @@ object MemoryValidation { case scala.util.Success(memorySize: MemorySize) => wrongAmountFormat.format(memorySize.amount).invalidNel case scala.util.Failure(throwable) => - missingFormat.format(throwable.getMessage).invalidNel + wrongTypeFormat.format(throwable.getMessage).invalidNel } } @@ -65,10 +64,10 @@ class MemoryValidation extends RuntimeAttributesValidation[MemorySize] { override def coercion = Seq(WdlIntegerType, WdlStringType) - override protected def validateValue = { + override protected def validateValue: PartialFunction[WdlValue, ErrorOr[MemorySize]] = { case WdlInteger(value) => MemoryValidation.validateMemoryInteger(value) case WdlString(value) => MemoryValidation.validateMemoryString(value) } - override def failureMessage = missingMessage + override def missingValueMessage: String = wrongTypeFormat.format("Not supported WDL type value") } diff --git a/backend/src/main/scala/cromwell/backend/validation/PrimitiveRuntimeAttributesValidation.scala b/backend/src/main/scala/cromwell/backend/validation/PrimitiveRuntimeAttributesValidation.scala index 16fed1f3d..aa07afcdc 100644 --- a/backend/src/main/scala/cromwell/backend/validation/PrimitiveRuntimeAttributesValidation.scala +++ b/backend/src/main/scala/cromwell/backend/validation/PrimitiveRuntimeAttributesValidation.scala @@ -1,51 +1,74 @@ -package cromwell.backend.impl.sfs.config +package cromwell.backend.validation import cats.syntax.validated._ -import cromwell.backend.validation.RuntimeAttributesValidation +import lenthall.validation.ErrorOr.ErrorOr import wdl4s.types._ -import wdl4s.values.{WdlBoolean, WdlFloat, WdlInteger, WdlString} +import wdl4s.values.{WdlBoolean, WdlFloat, WdlInteger, WdlPrimitive, WdlString, WdlValue} /** * Validates one of the wdl primitive types: Boolean, Float, Integer, or String. WdlFile is not supported. * * @tparam A The type of validated runtime attribute. + * @tparam B The type of coerced WdlValue. */ -sealed trait PrimitiveRuntimeAttributesValidation[A] extends RuntimeAttributesValidation[A] { +sealed trait PrimitiveRuntimeAttributesValidation[A, B <: WdlPrimitive] extends RuntimeAttributesValidation[A] { val wdlType: WdlPrimitiveType override def coercion = Seq(wdlType) + + override protected def validateExpression: PartialFunction[WdlValue, Boolean] = { + case wdlValue if wdlType.coerceRawValue(wdlValue).isSuccess => true + } + + override protected def missingValueMessage: String = + s"Expecting $key runtime attribute to be $typeString" + + protected def typeString = s"a ${wdlType.toWdlString}" + + override protected def validateValue: PartialFunction[WdlValue, ErrorOr[A]] = { + case value if wdlType.coerceRawValue(value).isSuccess => + validateCoercedValue(wdlType.coerceRawValue(value).get.asInstanceOf[B]) + case value if wdlType.coerceRawValue(value.valueString).isSuccess => + /* + NOTE: This case statement handles WdlString("true") coercing to WdlBoolean(true). + For some reason "true" as String is coercable... but not the WdlString. + */ + validateCoercedValue(wdlType.coerceRawValue(value.valueString).get.asInstanceOf[B]) + } + + protected def validateCoercedValue(wdlValue: B): ErrorOr[A] } class BooleanRuntimeAttributesValidation(override val key: String) extends - PrimitiveRuntimeAttributesValidation[Boolean] { + PrimitiveRuntimeAttributesValidation[Boolean, WdlBoolean] { override val wdlType = WdlBooleanType - override protected def validateValue = { - case WdlBoolean(value) => value.validNel - } + override protected def validateCoercedValue(wdlValue: WdlBoolean): ErrorOr[Boolean] = wdlValue.value.validNel } -class FloatRuntimeAttributesValidation(override val key: String) extends PrimitiveRuntimeAttributesValidation[Double] { +class FloatRuntimeAttributesValidation(override val key: String) extends + PrimitiveRuntimeAttributesValidation[Double, WdlFloat] { + override val wdlType = WdlFloatType - override protected def validateValue = { - case WdlFloat(value) => value.validNel - } + override protected def validateCoercedValue(wdlValue: WdlFloat): ErrorOr[Double] = wdlValue.value.validNel } -class IntRuntimeAttributesValidation(override val key: String) extends PrimitiveRuntimeAttributesValidation[Int] { +class IntRuntimeAttributesValidation(override val key: String) extends + PrimitiveRuntimeAttributesValidation[Int, WdlInteger] { + override val wdlType = WdlIntegerType - override protected def validateValue = { - case WdlInteger(value) => value.toInt.validNel - } + override protected def validateCoercedValue(wdlValue: WdlInteger): ErrorOr[Int] = wdlValue.value.toInt.validNel + + override protected def typeString: String = "an Integer" } -class StringRuntimeAttributesValidation(override val key: String) extends PrimitiveRuntimeAttributesValidation[String] { +class StringRuntimeAttributesValidation(override val key: String) extends + PrimitiveRuntimeAttributesValidation[String, WdlString] { + override val wdlType = WdlStringType - override protected def validateValue = { - case WdlString(value) => value.validNel - } + override protected def validateCoercedValue(wdlValue: WdlString): ErrorOr[String] = wdlValue.value.validNel } diff --git a/backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala b/backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala index 1adbc254f..322fe0e43 100644 --- a/backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala +++ b/backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala @@ -1,54 +1,51 @@ package cromwell.backend.validation +import cats.data.{NonEmptyList, Validated} import cats.syntax.validated._ -import wdl4s.expression.PureStandardLibraryFunctions import cromwell.backend.{MemorySize, RuntimeAttributeDefinition} import lenthall.validation.ErrorOr._ import org.slf4j.Logger -import wdl4s.WdlExpression -import wdl4s.WdlExpression._ +import wdl4s.expression.PureStandardLibraryFunctions import wdl4s.types.{WdlBooleanType, WdlIntegerType, WdlType} import wdl4s.values._ +import wdl4s.{NoLookup, WdlExpression} import scala.util.{Failure, Success} object RuntimeAttributesValidation { - def warnUnrecognized(actual: Set[String], expected: Set[String], logger: Logger) = { + def warnUnrecognized(actual: Set[String], expected: Set[String], logger: Logger): Unit = { val unrecognized = actual.diff(expected).mkString(", ") if (unrecognized.nonEmpty) logger.warn(s"Unrecognized runtime attribute keys: $unrecognized") } def validateDocker(docker: Option[WdlValue], onMissingKey: => ErrorOr[Option[String]]): ErrorOr[Option[String]] = { - validateWithValidation(docker, DockerValidation.optional, onMissingKey, DockerValidation.missingMessage) + validateWithValidation(docker, DockerValidation.optional, onMissingKey) } def validateFailOnStderr(value: Option[WdlValue], onMissingKey: => ErrorOr[Boolean]): ErrorOr[Boolean] = { - validateWithValidation(value, FailOnStderrValidation.default, onMissingKey, FailOnStderrValidation.missingMessage) + validateWithValidation(value, FailOnStderrValidation.default, onMissingKey) } def validateContinueOnReturnCode(value: Option[WdlValue], onMissingKey: => ErrorOr[ContinueOnReturnCode]): ErrorOr[ContinueOnReturnCode] = { - validateWithValidation(value, ContinueOnReturnCodeValidation.default, onMissingKey, - ContinueOnReturnCodeValidation.missingMessage) + validateWithValidation(value, ContinueOnReturnCodeValidation.default, onMissingKey) } def validateMemory(value: Option[WdlValue], onMissingKey: => ErrorOr[MemorySize]): ErrorOr[MemorySize] = { - validateWithValidation(value, MemoryValidation.instance, onMissingKey, - MemoryValidation.missingFormat.format("Not supported WDL type value")) + validateWithValidation(value, MemoryValidation.instance, onMissingKey) } def validateCpu(cpu: Option[WdlValue], onMissingKey: => ErrorOr[Int]): ErrorOr[Int] = { - validateWithValidation(cpu, CpuValidation.default, onMissingKey, CpuValidation.missingMessage) + validateWithValidation(cpu, CpuValidation.default, onMissingKey) } private def validateWithValidation[T](valueOption: Option[WdlValue], validation: RuntimeAttributesValidation[T], - onMissingValue: => ErrorOr[T], - missingValidationMessage: String): ErrorOr[T] = { + onMissingValue: => ErrorOr[T]): ErrorOr[T] = { valueOption match { case Some(value) => - validation.validateValue.applyOrElse(value, (_: Any) => missingValidationMessage.invalidNel) + validation.validateValue.applyOrElse(value, (_: Any) => validation.invalidValueFailure(value)) case None => onMissingValue } } @@ -78,46 +75,62 @@ object RuntimeAttributesValidation { def withDefault[ValidatedType](validation: RuntimeAttributesValidation[ValidatedType], default: WdlValue): RuntimeAttributesValidation[ValidatedType] = { new RuntimeAttributesValidation[ValidatedType] { - override def key = validation.key + override def key: String = validation.key + + override def coercion: Traversable[WdlType] = validation.coercion - override def coercion = validation.coercion + override protected def validateValue: PartialFunction[WdlValue, ErrorOr[ValidatedType]] = + validation.validateValuePackagePrivate - override protected def validateValue = validation.validateValuePackagePrivate + override protected def validateExpression: PartialFunction[WdlValue, Boolean] = + validation.validateExpressionPackagePrivate - override protected def validateExpression = validation.validateExpressionPackagePrivate + override protected def invalidValueMessage(value: WdlValue): String = + validation.invalidValueMessagePackagePrivate(value) - override def staticDefaultOption = Option(default) + override protected def missingValueMessage: String = validation.missingValueMessage - override protected def failureMessage = validation.failureMessagePackagePrivate + override protected def usedInCallCaching: Boolean = validation.usedInCallCachingPackagePrivate + + override protected def staticDefaultOption = Option(default) } } def optional[ValidatedType](validation: RuntimeAttributesValidation[ValidatedType]): OptionalRuntimeAttributesValidation[ValidatedType] = { new OptionalRuntimeAttributesValidation[ValidatedType] { - override def key = validation.key + override def key: String = validation.key + + override def coercion: Traversable[WdlType] = validation.coercion - override def coercion = validation.coercion + override protected def validateOption: PartialFunction[WdlValue, ErrorOr[ValidatedType]] = + validation.validateValuePackagePrivate - override protected def validateOption = validation.validateValuePackagePrivate + override protected def validateExpression: PartialFunction[WdlValue, Boolean] = + validation.validateExpressionPackagePrivate - override protected def validateExpression = validation.validateExpressionPackagePrivate + override protected def invalidValueMessage(value: WdlValue): String = + validation.invalidValueMessagePackagePrivate(value) - override protected def failureMessage = validation.failureMessagePackagePrivate + override protected def missingValueMessage: String = validation.missingValueMessage + + override protected def usedInCallCaching: Boolean = validation.usedInCallCachingPackagePrivate } } /** - * Returns the value from the attributes, unpacking options. + * Returns the value from the attributes, unpacking options, and converting them to string values suitable for + * storage in metadata. * * @param validatedRuntimeAttributes The values to search. * @return The keys and extracted values. */ - def extract(validatedRuntimeAttributes: ValidatedRuntimeAttributes): Map[String, Any] = { + def toMetadataStrings(validatedRuntimeAttributes: ValidatedRuntimeAttributes): Map[String, String] = { val attributeOptions: Map[String, Option[Any]] = validatedRuntimeAttributes.attributes.mapValues(unpackOption) - val attributes = attributeOptions collect { - case (name, Some(value)) => (name, value) + val attributes: Map[String, String] = attributeOptions collect { + case (name, Some(values: Traversable[_])) => (name, values.mkString(",")) + case (name, Some(value)) => (name, value.toString) } attributes @@ -195,23 +208,6 @@ object RuntimeAttributesValidation { case _ => Option(value.asInstanceOf[A]) } } - - /** - * Converts a RuntimeAttributesValidation to a RuntimeAttributeDefinition. - * - * @param validation RuntimeAttributesValidation - * @return RuntimeAttributeDefinition - */ - def toRuntimeAttributeDefinition(validation: RuntimeAttributesValidation[_]): RuntimeAttributeDefinition = { - val name = validation.key - val default = validation.staticDefaultOption - import cromwell.backend.validation.RuntimeAttributesKeys._ - val usedInCallCaching = name match { - case DockerKey | ContinueOnReturnCodeKey | FailOnStderrKey => true - case _ => false - } - RuntimeAttributeDefinition(name, default, usedInCallCaching) - } } /** @@ -247,7 +243,7 @@ trait RuntimeAttributesValidation[ValidatedType] { * * @return the value for when there is no wdl value. */ - protected def validateNone: ErrorOr[ValidatedType] = failureWithMessage + protected def validateNone: ErrorOr[ValidatedType] = missingValueFailure /** * Returns true if the value can be validated. @@ -268,31 +264,50 @@ trait RuntimeAttributesValidation[ValidatedType] { * * @return the optional default value when no other is specified. */ - def staticDefaultOption: Option[WdlValue] = None + protected def staticDefaultOption: Option[WdlValue] = None /** * Returns message to return when a value is invalid. * + * By default returns the missingValueMessage. + * * @return Message to return when a value is invalid. */ - protected def failureMessage: String = s"Expecting $key runtime attribute to be a type in $coercion" + protected def invalidValueMessage(value: WdlValue): String = missingValueMessage + + /** + * Utility method to wrap the invalidValueMessage in an ErrorOr. + * + * @return Wrapped invalidValueMessage. + */ + protected final def invalidValueFailure(value: WdlValue): ErrorOr[ValidatedType] = + invalidValueMessage(value).invalidNel /** - * Utility method to wrap the failureMessage in an ErrorOr. + * Returns message to return when a value is missing. * - * @return Wrapped failureMessage. + * @return Message to return when a value is missing. */ - protected final lazy val failureWithMessage: ErrorOr[ValidatedType] = failureMessage.invalidNel + protected def missingValueMessage: String = s"Expecting $key runtime attribute to be a type in $coercion" + + /** + * Utility method to wrap the missingValueMessage in an ErrorOr. + * + * @return Wrapped missingValueMessage. + */ + protected final lazy val missingValueFailure: ErrorOr[ValidatedType] = missingValueMessage.invalidNel /** * Runs this validation on the value matching key. * + * NOTE: The values passed to this method should already be evaluated instances of WdlValue, and not WdlExpression. + * * @param values The full set of values. * @return The error or valid value for this key. */ def validate(values: Map[String, WdlValue]): ErrorOr[ValidatedType] = { values.get(key) match { - case Some(value) => validateValue.applyOrElse(value, (_: Any) => failureWithMessage) + case Some(value) => validateValue.applyOrElse(value, (_: Any) => invalidValueFailure(value)) case None => validateNone } } @@ -308,6 +323,9 @@ trait RuntimeAttributesValidation[ValidatedType] { * * With our `key` as the key in the map, one can return this function as the value in the map. * + * NOTE: If there is an attempt lookup a value within a WdlExpression, or a WdlExpression fails to evaluate for any + * reason, this method will simply return true. + * * @param wdlExpressionMaybe The optional expression. * @return True if the expression may be evaluated. */ @@ -315,36 +333,28 @@ trait RuntimeAttributesValidation[ValidatedType] { wdlExpressionMaybe match { case None => staticDefaultOption.isDefined || validateNone.isValid case Some(wdlExpression: WdlExpression) => - /* - TODO: BUG: - - Using `wdl4s.NoLookup` with the following options causes the following exception: - - command: - sbt -J-Dbackend.default=SGE 'run run test_wdl/test.wdl - test_wdl/test.default.options' - - options: - { "default_runtime_attributes": { "sge_queue": "fromoptions" } } - - exception: - java.lang.RuntimeException: Expression evaluation failed due to java.lang.UnsupportedOperationException: - No identifiers should be looked up: fromoptions: WdlExpression( WdlString(value) - wdlExpression.evaluate(wdlStringLookup, PureStandardLibraryFunctions) match { + wdlExpression.evaluate(NoLookup, PureStandardLibraryFunctions) match { case Success(wdlValue) => validateExpression.applyOrElse(wdlValue, (_: Any) => false) - case Failure(throwable) => - throw new RuntimeException(s"Expression evaluation failed due to $throwable: $wdlExpression", throwable) + case Failure(_) => true // If we can't evaluate it, we'll let it pass for now... } case Some(wdlValue) => validateExpression.applyOrElse(wdlValue, (_: Any) => false) } } /** + * Used to convert this instance to a `RuntimeAttributeDefinition`. + * + * @see [[RuntimeAttributeDefinition.usedInCallCaching]] + * @return Value for [[RuntimeAttributeDefinition.usedInCallCaching]]. + */ + protected def usedInCallCaching: Boolean = false + + /** + * Returns this as an instance of a runtime attribute definition. + */ + final lazy val runtimeAttributeDefinition = RuntimeAttributeDefinition(key, staticDefaultOption, usedInCallCaching) + + /** * Returns an optional version of this validation. */ final lazy val optional: OptionalRuntimeAttributesValidation[ValidatedType] = @@ -356,7 +366,8 @@ trait RuntimeAttributesValidation[ValidatedType] { * @param wdlValue The default wdl value. * @return The new version of this validation. */ - final def withDefault(wdlValue: WdlValue) = RuntimeAttributesValidation.withDefault(this, wdlValue) + final def withDefault(wdlValue: WdlValue): RuntimeAttributesValidation[ValidatedType] = + RuntimeAttributesValidation.withDefault(this, wdlValue) /* Methods below provide aliases to expose protected methods to the package. @@ -365,11 +376,15 @@ trait RuntimeAttributesValidation[ValidatedType] { access the protected values, except the `validation` package that uses these back doors. */ - private[validation] lazy val validateValuePackagePrivate = validateValue + private[validation] final lazy val validateValuePackagePrivate = validateValue + + private[validation] final lazy val validateExpressionPackagePrivate = validateExpression - private[validation] lazy val validateExpressionPackagePrivate = validateExpression + private[validation] final def invalidValueMessagePackagePrivate(value: WdlValue) = invalidValueMessage(value) - private[validation] lazy val failureMessagePackagePrivate = failureMessage + private[validation] final lazy val missingValueMessagePackagePrivate = missingValueMessage + + private[validation] final lazy val usedInCallCachingPackagePrivate = usedInCallCaching } /** @@ -389,10 +404,12 @@ trait OptionalRuntimeAttributesValidation[ValidatedType] extends RuntimeAttribut protected def validateOption: PartialFunction[WdlValue, ErrorOr[ValidatedType]] override final protected lazy val validateValue = new PartialFunction[WdlValue, ErrorOr[Option[ValidatedType]]] { - override def isDefinedAt(wdlValue: WdlValue) = validateOption.isDefinedAt(wdlValue) + override def isDefinedAt(wdlValue: WdlValue): Boolean = validateOption.isDefinedAt(wdlValue) - override def apply(wdlValue: WdlValue) = validateOption.apply(wdlValue).map(Option.apply) + override def apply(wdlValue: WdlValue): Validated[NonEmptyList[String], Option[ValidatedType]] = { + validateOption.apply(wdlValue).map(Option.apply) + } } - override final protected lazy val validateNone = None.validNel[String] + override final protected lazy val validateNone: ErrorOr[None.type] = None.validNel[String] } diff --git a/backend/src/main/scala/cromwell/backend/validation/ValidatedRuntimeAttributesBuilder.scala b/backend/src/main/scala/cromwell/backend/validation/ValidatedRuntimeAttributesBuilder.scala index ad3c2cb57..ed3981817 100644 --- a/backend/src/main/scala/cromwell/backend/validation/ValidatedRuntimeAttributesBuilder.scala +++ b/backend/src/main/scala/cromwell/backend/validation/ValidatedRuntimeAttributesBuilder.scala @@ -6,6 +6,7 @@ import cromwell.backend.RuntimeAttributeDefinition import lenthall.exception.MessageAggregation import lenthall.validation.ErrorOr._ import org.slf4j.Logger +import wdl4s.types.WdlType import wdl4s.values.WdlValue final case class ValidatedRuntimeAttributes(attributes: Map[String, Any]) @@ -29,20 +30,23 @@ trait ValidatedRuntimeAttributesBuilder { /** * Returns a mapping of the validations: RuntimeAttributesValidation each converted to a RuntimeAttributeDefinition. */ - final lazy val definitions: Seq[RuntimeAttributeDefinition] = { - validations map RuntimeAttributesValidation.toRuntimeAttributeDefinition + final lazy val definitions: Seq[RuntimeAttributeDefinition] = validations.map(_.runtimeAttributeDefinition) + + /** + * Returns validators suitable for BackendWorkflowInitializationActor.runtimeAttributeValidators. + */ + final lazy val validatorMap: Map[String, (Option[WdlValue]) => Boolean] = { + validations.map(validation => + validation.key -> validation.validateOptionalExpression _ + ).toMap } /** - * Returns the additional validations that should be used during value parsing. - * - * For example, sometimes docker might not be supported, BUT we want to still validate the value if specified. - * - * In that case, return the validation here. - * - * @return the additional validations that should be used during value parsing. + * Returns a map of coercions suitable for RuntimeAttributesDefault.workflowOptionsDefault. */ - protected def unsupportedExtraValidations: Seq[OptionalRuntimeAttributesValidation[_]] = Seq.empty + final lazy val coercionMap: Map[String, Traversable[WdlType]] = { + validations.map(validation => validation.key -> validation.coercion).toMap + } def unsupportedKeys(keys: Seq[String]): Seq[String] = keys.diff(validationKeys) @@ -63,9 +67,8 @@ trait ValidatedRuntimeAttributesBuilder { } private def validate(values: Map[String, WdlValue]): ErrorOr[ValidatedRuntimeAttributes] = { - val validationsForValues = validations ++ unsupportedExtraValidations val listOfKeysToErrorOrAnys: List[(String, ErrorOr[Any])] = - validationsForValues.map(validation => validation.key -> validation.validate(values)).toList + validations.map(validation => validation.key -> validation.validate(values)).toList val listOfErrorOrKeysToAnys: List[ErrorOr[(String, Any)]] = listOfKeysToErrorOrAnys map { case (key, errorOrAny) => errorOrAny map { any => (key, any) } diff --git a/backend/src/test/scala/cromwell/backend/BackendWorkflowInitializationActorSpec.scala b/backend/src/test/scala/cromwell/backend/BackendWorkflowInitializationActorSpec.scala new file mode 100644 index 000000000..82e62d641 --- /dev/null +++ b/backend/src/test/scala/cromwell/backend/BackendWorkflowInitializationActorSpec.scala @@ -0,0 +1,204 @@ +package cromwell.backend + +import akka.actor.ActorRef +import akka.testkit.TestActorRef +import cromwell.backend.validation.{ContinueOnReturnCodeFlag, ContinueOnReturnCodeSet, ContinueOnReturnCodeValidation, RuntimeAttributesKeys} +import cromwell.core.{TestKitSuite, WorkflowOptions} +import org.scalatest.prop.TableDrivenPropertyChecks +import org.scalatest.{FlatSpecLike, Matchers} +import wdl4s.types._ +import wdl4s.values.{WdlArray, WdlBoolean, WdlFloat, WdlInteger, WdlString, WdlValue} +import wdl4s.{TaskCall, WdlExpression} + +import scala.concurrent.Future +import scala.util.Try + +class BackendWorkflowInitializationActorSpec extends TestKitSuite("BackendWorkflowInitializationActorSpec") + with FlatSpecLike with Matchers with TableDrivenPropertyChecks { + + behavior of "BackendWorkflowInitializationActorSpec" + + val testPredicateBackendWorkflowInitializationActorRef: + TestActorRef[TestPredicateBackendWorkflowInitializationActor] = + TestActorRef[TestPredicateBackendWorkflowInitializationActor] + + val testPredicateBackendWorkflowInitializationActor: + TestPredicateBackendWorkflowInitializationActor = + testPredicateBackendWorkflowInitializationActorRef.underlyingActor + + val testContinueOnReturnCode: (Option[WdlValue]) => Boolean = { + testPredicateBackendWorkflowInitializationActor.continueOnReturnCodePredicate(valueRequired = false) + } + + it should "continueOnReturnCodePredicate" in { + testContinueOnReturnCode(None) should be(true) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(None) should be(true) + + val booleanRows = Table( + "value", + true, + false + ) + + val integerRows = Table( + "value", + -1, + 0, + 1, + 1024 + ) + + val expressionRows = Table( + "expression", + "read_int(\"bad file\")" + ) + + val invalidWdlValueRows = Table( + "wdlValue", + WdlString(""), + WdlString("z"), + WdlFloat(0.0D), + WdlArray(WdlArrayType(WdlBooleanType), Seq(WdlBoolean(true))), + WdlArray(WdlArrayType(WdlFloatType), Seq(WdlFloat(0.0D))) + ) + + forAll(booleanRows) { value => + val wdlValue = WdlBoolean(value) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + val valid = + ContinueOnReturnCodeValidation.default.validate(Map(RuntimeAttributesKeys.ContinueOnReturnCodeKey -> wdlValue)) + valid.isValid should be(result) + valid.toEither.right.get should be(ContinueOnReturnCodeFlag(value)) + } + + forAll(booleanRows) { value => + val wdlValue = WdlString(value.toString) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + val valid = + ContinueOnReturnCodeValidation.default.validate(Map(RuntimeAttributesKeys.ContinueOnReturnCodeKey -> wdlValue)) + valid.isValid should be(result) + valid.toEither.right.get should be(ContinueOnReturnCodeFlag(value)) + } + + forAll(booleanRows) { value => + val wdlValue = WdlExpression.fromString(value.toString) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + // NOTE: expressions are never valid to validate + } + + forAll(integerRows) { value => + val wdlValue = WdlInteger(value) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + val valid = + ContinueOnReturnCodeValidation.default.validate(Map(RuntimeAttributesKeys.ContinueOnReturnCodeKey -> wdlValue)) + valid.isValid should be(result) + valid.toEither.right.get should be(ContinueOnReturnCodeSet(Set(value))) + } + + forAll(integerRows) { value => + val wdlValue = WdlString(value.toString) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + val valid = + ContinueOnReturnCodeValidation.default.validate(Map(RuntimeAttributesKeys.ContinueOnReturnCodeKey -> wdlValue)) + valid.isValid should be(result) + valid.toEither.right.get should be(ContinueOnReturnCodeSet(Set(value))) + } + + forAll(integerRows) { value => + val wdlValue = WdlExpression.fromString(value.toString) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + // NOTE: expressions are never valid to validate + } + + forAll(integerRows) { value => + val wdlValue = WdlArray(WdlArrayType(WdlIntegerType), Seq(WdlInteger(value))) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + val valid = + ContinueOnReturnCodeValidation.default.validate(Map(RuntimeAttributesKeys.ContinueOnReturnCodeKey -> wdlValue)) + valid.isValid should be(result) + valid.toEither.right.get should be(ContinueOnReturnCodeSet(Set(value))) + } + + forAll(integerRows) { value => + val wdlValue = WdlArray(WdlArrayType(WdlStringType), Seq(WdlString(value.toString))) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + val valid = + ContinueOnReturnCodeValidation.default.validate(Map(RuntimeAttributesKeys.ContinueOnReturnCodeKey -> wdlValue)) + valid.isValid should be(result) + valid.toEither.right.get should be(ContinueOnReturnCodeSet(Set(value))) + } + + forAll(integerRows) { value => + val wdlValue = WdlArray(WdlArrayType(WdlExpressionType), Seq(WdlExpression.fromString(value.toString))) + val result = false + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + // NOTE: expressions are never valid to validate + } + + forAll(expressionRows) { expression => + val wdlValue = WdlExpression.fromString(expression) + val result = true + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + // NOTE: expressions are never valid to validate + } + + forAll(invalidWdlValueRows) { wdlValue => + val result = false + testContinueOnReturnCode(Option(wdlValue)) should be(result) + ContinueOnReturnCodeValidation.default.validateOptionalExpression(Option(wdlValue)) should be(result) + val valid = + ContinueOnReturnCodeValidation.default.validate(Map(RuntimeAttributesKeys.ContinueOnReturnCodeKey -> wdlValue)) + valid.isValid should be(result) + valid.toEither.left.get.toList should contain theSameElementsAs List( + "Expecting continueOnReturnCode runtime attribute to be either a Boolean, a String 'true' or 'false', or an Array[Int]" + ) + } + + } + +} + +class TestPredicateBackendWorkflowInitializationActor extends BackendWorkflowInitializationActor { + override val serviceRegistryActor: ActorRef = context.system.deadLetters + + override def calls: Set[TaskCall] = throw new NotImplementedError("calls") + + override protected def runtimeAttributeValidators: Map[String, (Option[WdlValue]) => Boolean] = + throw new NotImplementedError("runtimeAttributeValidators") + + override protected def coerceDefaultRuntimeAttributes(options: WorkflowOptions): Try[Map[String, WdlValue]] = + throw new NotImplementedError("coerceDefaultRuntimeAttributes") + + override def beforeAll(): Future[Option[BackendInitializationData]] = throw new NotImplementedError("beforeAll") + + override def validate(): Future[Unit] = throw new NotImplementedError("validate") + + override protected def workflowDescriptor: BackendWorkflowDescriptor = + throw new NotImplementedError("workflowDescriptor") + + override protected def configurationDescriptor: BackendConfigurationDescriptor = + throw new NotImplementedError("configurationDescriptor") + + override def continueOnReturnCodePredicate(valueRequired: Boolean) + (wdlExpressionMaybe: Option[WdlValue]): Boolean = { + super.continueOnReturnCodePredicate(valueRequired)(wdlExpressionMaybe) + } +} diff --git a/backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala b/backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala index c257ffa4d..b116aae72 100644 --- a/backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala +++ b/backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala @@ -1,4 +1,4 @@ -package cromwell.backend.sfs +package cromwell.backend.standard import cromwell.backend.RuntimeAttributeDefinition import cromwell.backend.validation.RuntimeAttributesKeys._ @@ -10,9 +10,9 @@ import org.specs2.mock.Mockito import spray.json.{JsArray, JsBoolean, JsNumber, JsObject, JsValue} import wdl4s.values.{WdlBoolean, WdlInteger, WdlString, WdlValue} -class SharedFileSystemValidatedRuntimeAttributesBuilderSpec extends WordSpecLike with Matchers with Mockito { +class StandardValidatedRuntimeAttributesBuilderSpec extends WordSpecLike with Matchers with Mockito { - val HelloWorld = + val HelloWorld: String = s""" |task hello { | String addressee = "you" @@ -37,7 +37,7 @@ class SharedFileSystemValidatedRuntimeAttributesBuilderSpec extends WordSpecLike FailOnStderrKey -> false, ContinueOnReturnCodeKey -> ContinueOnReturnCodeSet(Set(0))) - def workflowOptionsWithDefaultRuntimeAttributes(defaults: Map[String, JsValue]) = { + def workflowOptionsWithDefaultRuntimeAttributes(defaults: Map[String, JsValue]): WorkflowOptions = { WorkflowOptions(JsObject(Map("default_runtime_attributes" -> JsObject(defaults)))) } @@ -75,7 +75,18 @@ class SharedFileSystemValidatedRuntimeAttributesBuilderSpec extends WordSpecLike message should include("Unrecognized runtime attribute keys: docker") } - "log a warning and fail to validate an invalid Docker entry" in { + "log a warning and validate an invalid Docker entry" in { + /* + NOTE: The behavior used to be: when present, a "docker" runtime attribute would always be validated to ensure that + the value of the runtime attribute was a String-- even if the actual runtime attribute was unsupported by the + backend! NOW, if the backend doesn't support docker, and the runtime attributes contain say as an invalid integer, + say `{ docker: 1 }`, the validation will now succeed. If we want to change this, the git history contains a + concept of validated-yet-still-unsupported attributes. One could use this behavior to validate other standard + attributes such as CPU, Memory, etc.-- checking their syntax even when they are unsupported by the current + backend. + + https://github.com/broadinstitute/cromwell/blob/a4a952de33f6d1ef646be51d298c3d613a8cce5f/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemValidatedRuntimeAttributesBuilder.scala + */ val expectedRuntimeAttributes = defaultRuntimeAttributes val runtimeAttributes = Map("docker" -> WdlInteger(1)) val mockWarnings = new MockWarnings @@ -129,7 +140,7 @@ class SharedFileSystemValidatedRuntimeAttributesBuilderSpec extends WordSpecLike */ class MockWarnings() { var warnings: Seq[String] = Seq.empty - val mockLogger = mock[Logger] + val mockLogger: Logger = mock[Logger] mockLogger.warn(anyString).answers { result => result match { case message: String => @@ -139,8 +150,8 @@ class SharedFileSystemValidatedRuntimeAttributesBuilderSpec extends WordSpecLike } } - val defaultLogger = LoggerFactory.getLogger(classOf[SharedFileSystemValidatedRuntimeAttributesBuilderSpec]) - val emptyWorkflowOptions = WorkflowOptions.fromMap(Map.empty).get + val defaultLogger: Logger = LoggerFactory.getLogger(classOf[StandardValidatedRuntimeAttributesBuilderSpec]) + val emptyWorkflowOptions: WorkflowOptions = WorkflowOptions.fromMap(Map.empty).get private def assertRuntimeAttributesSuccessfulCreation(runtimeAttributes: Map[String, WdlValue], expectedRuntimeAttributes: Map[String, Any], @@ -149,9 +160,9 @@ class SharedFileSystemValidatedRuntimeAttributesBuilderSpec extends WordSpecLike logger: Logger = defaultLogger): Unit = { val builder = if (includeDockerSupport) { - SharedFileSystemValidatedRuntimeAttributesBuilder.default.withValidation(DockerValidation.optional) + StandardValidatedRuntimeAttributesBuilder.default.withValidation(DockerValidation.optional) } else { - SharedFileSystemValidatedRuntimeAttributesBuilder.default + StandardValidatedRuntimeAttributesBuilder.default } val runtimeAttributeDefinitions = builder.definitions.toSet val addDefaultsToAttributes = RuntimeAttributeDefinition.addDefaultsToAttributes(runtimeAttributeDefinitions, workflowOptions) _ @@ -178,9 +189,9 @@ class SharedFileSystemValidatedRuntimeAttributesBuilderSpec extends WordSpecLike logger: Logger = defaultLogger): Unit = { val thrown = the[RuntimeException] thrownBy { val builder = if (supportsDocker) { - SharedFileSystemValidatedRuntimeAttributesBuilder.default.withValidation(DockerValidation.optional) + StandardValidatedRuntimeAttributesBuilder.default.withValidation(DockerValidation.optional) } else { - SharedFileSystemValidatedRuntimeAttributesBuilder.default + StandardValidatedRuntimeAttributesBuilder.default } val runtimeAttributeDefinitions = builder.definitions.toSet val addDefaultsToAttributes = RuntimeAttributeDefinition.addDefaultsToAttributes(runtimeAttributeDefinitions, workflowOptions) _ diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActor.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActor.scala index 447310711..dc24e4c3d 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActor.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActor.scala @@ -3,21 +3,18 @@ package cromwell.backend.impl.jes import java.net.SocketTimeoutException import java.nio.file.{Path, Paths} -import akka.actor.{Actor, ActorLogging, ActorRef} +import akka.actor.ActorRef import better.files._ import com.google.api.client.googleapis.json.GoogleJsonResponseException import com.google.cloud.storage.contrib.nio.CloudStoragePath -import cromwell.backend.BackendJobExecutionActor.BackendJobExecutionResponse import cromwell.backend._ import cromwell.backend.async.{AbortedExecutionHandle, ExecutionHandle, FailedNonRetryableExecutionHandle, FailedRetryableExecutionHandle, PendingExecutionHandle, SuccessfulExecutionHandle} import cromwell.backend.impl.jes.RunStatus.TerminalRunStatus import cromwell.backend.impl.jes.io._ import cromwell.backend.impl.jes.statuspolling.JesPollingActorClient import cromwell.backend.standard.{StandardAsyncExecutionActor, StandardAsyncExecutionActorParams, StandardAsyncJob} -import cromwell.backend.validation.ContinueOnReturnCode import cromwell.backend.wdl.OutputEvaluator import cromwell.core._ -import cromwell.core.logging.JobLogging import cromwell.core.path.PathFactory._ import cromwell.core.path.PathImplicits._ import cromwell.core.path.proxy.PathProxy @@ -27,25 +24,13 @@ import wdl4s.expression.{NoFunctions, WdlFunctions} import wdl4s.values._ import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future, Promise} +import scala.concurrent.{ExecutionContext, Future} import scala.language.postfixOps import scala.util.{Failure, Success, Try} -case class JesAsyncExecutionActorParams -( - override val jobDescriptor: BackendJobDescriptor, - jesConfiguration: JesConfiguration, - jesBackendInitializationData: JesBackendInitializationData, - override val serviceRegistryActor: ActorRef, - jesBackendSingletonActorOption: Option[ActorRef], - override val completionPromise: Promise[BackendJobExecutionResponse] -) extends StandardAsyncExecutionActorParams { - override val jobIdKey: String = JesJobExecutionActor.JesOperationIdKey - override val configurationDescriptor: BackendConfigurationDescriptor = jesConfiguration.configurationDescriptor - override val backendInitializationDataOption: Option[BackendInitializationData] = Option(jesBackendInitializationData) -} - object JesAsyncBackendJobExecutionActor { + val JesOperationIdKey = "__jes_operation_id" + object WorkflowOptionKeys { val MonitoringScript = "monitoring_script" val GoogleProject = "google_project" @@ -58,20 +43,14 @@ object JesAsyncBackendJobExecutionActor { private val ExtraConfigParamName = "__extra_config_gcs_path" } -class JesAsyncBackendJobExecutionActor(val jesParams: JesAsyncExecutionActorParams) - extends Actor with ActorLogging with BackendJobLifecycleActor with StandardAsyncExecutionActor - with JesJobCachingActorHelper with JobLogging with JesPollingActorClient { +class JesAsyncBackendJobExecutionActor(override val standardParams: StandardAsyncExecutionActorParams) + extends BackendJobLifecycleActor with StandardAsyncExecutionActor with JesJobCachingActorHelper + with JesPollingActorClient { import JesAsyncBackendJobExecutionActor._ - override val standardParams: StandardAsyncExecutionActorParams = jesParams - override val jesConfiguration: JesConfiguration = jesParams.jesConfiguration - override val initializationData: JesBackendInitializationData = { - backendInitializationDataAs[JesBackendInitializationData] - } - val jesBackendSingletonActor: ActorRef = - jesParams.jesBackendSingletonActorOption.getOrElse( + standardParams.backendSingletonActorOption.getOrElse( throw new RuntimeException("JES Backend actor cannot exist without the JES backend singleton actor")) override type StandardAsyncRunInfo = Run @@ -86,10 +65,6 @@ class JesAsyncBackendJobExecutionActor(val jesParams: JesAsyncExecutionActorPara override lazy val executeOrRecoverBackOff = SimpleExponentialBackoff( initialInterval = 3 seconds, maxInterval = 20 seconds, multiplier = 1.1) - override lazy val workflowDescriptor: BackendWorkflowDescriptor = jobDescriptor.workflowDescriptor - - lazy val call: TaskCall = jobDescriptor.key.call - override lazy val retryable: Boolean = jobDescriptor.key.attempt <= runtimeAttributes.preemptible private lazy val cmdInput = JesFileInput(ExecParamName, jesCallPaths.script.toRealString, Paths.get(jesCallPaths.scriptFilename), workingDisk) @@ -230,14 +205,6 @@ class JesAsyncBackendJobExecutionActor(val jesParams: JesAsyncExecutionActorPara ) } - override lazy val remoteStdErrPath: Path = jesStderrFile - - override lazy val remoteReturnCodePath: Path = returnCodeGcsPath - - override lazy val failOnStdErr: Boolean = runtimeAttributes.failOnStderr - - override lazy val continueOnReturnCode: ContinueOnReturnCode = runtimeAttributes.continueOnReturnCode - override lazy val commandLineFunctions: WdlFunctions[WdlValue] = backendEngineFunctions override lazy val commandLinePreProcessor: (EvaluatedTaskInputs) => Try[EvaluatedTaskInputs] = mapGcsValues diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala index 4ba914c34..0d3d07c31 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala @@ -1,7 +1,11 @@ package cromwell.backend.impl.jes import com.google.api.services.genomics.Genomics -import cromwell.backend.BackendInitializationData +import cromwell.backend.standard.StandardInitializationData -case class JesBackendInitializationData(workflowPaths: JesWorkflowPaths, genomics: Genomics) - extends BackendInitializationData +case class JesBackendInitializationData +( + override val workflowPaths: JesWorkflowPaths, + jesConfiguration: JesConfiguration, + genomics: Genomics +) extends StandardInitializationData(workflowPaths, JesRuntimeAttributes.runtimeAttributesBuilder) diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala index f61769379..00b5bfc96 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala @@ -1,82 +1,61 @@ package cromwell.backend.impl.jes -import java.nio.file.Path - -import akka.actor.{ActorRef, Props} -import com.typesafe.config.Config +import akka.actor.ActorRef import cromwell.backend._ import cromwell.backend.callcaching.FileHashingActor.FileHashingFunction import cromwell.backend.impl.jes.callcaching.JesBackendFileHashing -import cromwell.backend.validation.RuntimeAttributesKeys +import cromwell.backend.standard._ import cromwell.core.CallOutputs -import cromwell.core.Dispatcher.BackendDispatcher import wdl4s.TaskCall import wdl4s.expression.WdlStandardLibraryFunctions - case class JesBackendLifecycleActorFactory(name: String, configurationDescriptor: BackendConfigurationDescriptor) - extends BackendLifecycleActorFactory { + extends StandardLifecycleActorFactory { import JesBackendLifecycleActorFactory._ - val jesConfiguration = new JesConfiguration(configurationDescriptor) + override def initializationActorClass: Class[_ <: StandardInitializationActor] = classOf[JesInitializationActor] - override def workflowInitializationActorProps(workflowDescriptor: BackendWorkflowDescriptor, - calls: Set[TaskCall], - serviceRegistryActor: ActorRef): Option[Props] = { - Option(JesInitializationActor.props(workflowDescriptor, calls, jesConfiguration, serviceRegistryActor).withDispatcher(BackendDispatcher)) - } + override def asyncExecutionActorClass: Class[_ <: StandardAsyncExecutionActor] = + classOf[JesAsyncBackendJobExecutionActor] - override def jobExecutionActorProps(jobDescriptor: BackendJobDescriptor, - initializationData: Option[BackendInitializationData], - serviceRegistryActor: ActorRef, - backendSingletonActor: Option[ActorRef]): Props = { - // The `JesInitializationActor` will only return a non-`Empty` `JesBackendInitializationData` from a successful `beforeAll` - // invocation, so the `get` here is safe. - JesJobExecutionActor.props(jobDescriptor, jesConfiguration, initializationData.toJes.get, serviceRegistryActor, backendSingletonActor).withDispatcher(BackendDispatcher) - } + override def standardCacheHitCopyingActorOption: Option[Class[_ <: StandardCacheHitCopyingActor]] = + Option(classOf[JesCacheHitCopyingActor]) - override def cacheHitCopyingActorProps = Option(cacheHitCopyingActorInner _) + override def finalizationActorClassOption: Option[Class[_ <: StandardFinalizationActor]] = + Option(classOf[JesFinalizationActor]) - def cacheHitCopyingActorInner(jobDescriptor: BackendJobDescriptor, - initializationData: Option[BackendInitializationData], - serviceRegistryActor: ActorRef): Props = { - // The `JesInitializationActor` will only return a non-`Empty` `JesBackendInitializationData` from a successful `beforeAll` - // invocation, so the `get` here is safe. - JesCacheHitCopyingActor.props(jobDescriptor, jesConfiguration, initializationData.toJes.get, serviceRegistryActor).withDispatcher(BackendDispatcher) + override def jobIdKey: String = JesAsyncBackendJobExecutionActor.JesOperationIdKey + + val jesConfiguration = new JesConfiguration(configurationDescriptor) + + override def workflowInitializationActorParams(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], + serviceRegistryActor: ActorRef): StandardInitializationActorParams = { + JesInitializationActorParams(workflowDescriptor, calls, jesConfiguration, serviceRegistryActor) } - override def workflowFinalizationActorProps(workflowDescriptor: BackendWorkflowDescriptor, - calls: Set[TaskCall], - jobExecutionMap: JobExecutionMap, - workflowOutputs: CallOutputs, - initializationData: Option[BackendInitializationData]) = { + override def workflowFinalizationActorParams(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], + jobExecutionMap: JobExecutionMap, workflowOutputs: CallOutputs, + initializationDataOption: Option[BackendInitializationData]): + StandardFinalizationActorParams = { // The `JesInitializationActor` will only return a non-`Empty` `JesBackendInitializationData` from a successful `beforeAll` // invocation. HOWEVER, the finalization actor is created regardless of whether workflow initialization was successful // or not. So the finalization actor must be able to handle an empty `JesBackendInitializationData` option, and there is no // `.get` on the initialization data as there is with the execution or cache hit copying actor methods. - Option(JesFinalizationActor.props(workflowDescriptor, calls, jesConfiguration, jobExecutionMap, workflowOutputs, initializationData.toJes).withDispatcher(BackendDispatcher)) + JesFinalizationActorParams(workflowDescriptor, calls, jesConfiguration, jobExecutionMap, workflowOutputs, + initializationDataOption) } - override def runtimeAttributeDefinitions(initializationDataOption: Option[BackendInitializationData]) = staticRuntimeAttributeDefinitions + override def runtimeAttributeDefinitions(initializationDataOption: Option[BackendInitializationData]): + Set[RuntimeAttributeDefinition] = staticRuntimeAttributeDefinitions override def expressionLanguageFunctions(workflowDescriptor: BackendWorkflowDescriptor, jobKey: BackendJobDescriptorKey, initializationData: Option[BackendInitializationData]): WdlStandardLibraryFunctions = { - val jesCallPaths = initializationData.toJes.get.workflowPaths.toJobPaths(jobKey) + val jesCallPaths = initializationData.toJes.get.workflowPaths.toJobPaths(jobKey, workflowDescriptor) new JesExpressionFunctions(List(jesCallPaths.gcsPathBuilder), jesCallPaths.callContext) } - override def getExecutionRootPath(workflowDescriptor: BackendWorkflowDescriptor, backendConfig: Config, - initializationData: Option[BackendInitializationData]): Path = { - initializationData.toJes.get.workflowPaths.executionRoot - } - - override def getWorkflowExecutionRootPath(workflowDescriptor: BackendWorkflowDescriptor, backendConfig: Config, - initializationData: Option[BackendInitializationData]): Path = { - initializationData.toJes.get.workflowPaths.workflowRoot - } - override def backendSingletonActorProps = Option(JesBackendSingletonActor.props(jesConfiguration.qps)) override lazy val fileHashingFunction: Option[FileHashingFunction] = Option(FileHashingFunction(JesBackendFileHashing.getCrc32c)) @@ -90,21 +69,6 @@ object JesBackendLifecycleActorFactory { def toJes: Option[JesBackendInitializationData] = genericInitializationData collectFirst { case d: JesBackendInitializationData => d } } - val staticRuntimeAttributeDefinitions = { - import JesRuntimeAttributes._ - import RuntimeAttributesKeys._ - - Set( - RuntimeAttributeDefinition(DockerKey, None, usedInCallCaching = true), - RuntimeAttributeDefinition(ContinueOnReturnCodeKey, Option(staticDefaults(ContinueOnReturnCodeKey)), usedInCallCaching = true), - RuntimeAttributeDefinition(CpuKey, Option(staticDefaults(CpuKey)), usedInCallCaching = false), - RuntimeAttributeDefinition(FailOnStderrKey, Option(staticDefaults(FailOnStderrKey)), usedInCallCaching = true), - RuntimeAttributeDefinition(MemoryKey, Option(staticDefaults(MemoryKey)), usedInCallCaching = false), - RuntimeAttributeDefinition(DisksKey, Option(staticDefaults(DisksKey)), usedInCallCaching = false), - RuntimeAttributeDefinition(ZonesKey, Option(staticDefaults(ZonesKey)), usedInCallCaching = false), - RuntimeAttributeDefinition(PreemptibleKey, Option(staticDefaults(PreemptibleKey)), usedInCallCaching = false), - RuntimeAttributeDefinition(BootDiskSizeKey, Option(staticDefaults(BootDiskSizeKey)), usedInCallCaching = false), - RuntimeAttributeDefinition(NoAddressKey, Option(staticDefaults(NoAddressKey)), usedInCallCaching = false) - ) - } + val staticRuntimeAttributeDefinitions: Set[RuntimeAttributeDefinition] = + JesRuntimeAttributes.runtimeAttributesBuilder.definitions.toSet } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesCacheHitCopyingActor.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesCacheHitCopyingActor.scala index b37a9ff31..631eda1b8 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesCacheHitCopyingActor.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesCacheHitCopyingActor.scala @@ -2,35 +2,10 @@ package cromwell.backend.impl.jes import java.nio.file.Path -import akka.actor.{ActorRef, Props} -import cromwell.core.Dispatcher.BackendDispatcher -import cromwell.backend.callcaching.CacheHitDuplicating -import cromwell.backend.{BackendCacheHitCopyingActor, BackendConfigurationDescriptor, BackendJobDescriptor, BackendWorkflowDescriptor} +import cromwell.backend.standard.{StandardCacheHitCopyingActor, StandardCacheHitCopyingActorParams} import cromwell.core.path.PathCopier -import cromwell.core.logging.JobLogging -case class JesCacheHitCopyingActor(override val jobDescriptor: BackendJobDescriptor, - jesConfiguration: JesConfiguration, - initializationData: JesBackendInitializationData, - serviceRegistryActor: ActorRef) - extends BackendCacheHitCopyingActor with CacheHitDuplicating with JesJobCachingActorHelper with JobLogging { +case class JesCacheHitCopyingActor(override val standardParams: StandardCacheHitCopyingActorParams) + extends StandardCacheHitCopyingActor with JesJobCachingActorHelper { override protected def duplicate(source: Path, destination: Path): Unit = PathCopier.copy(source, destination).get - - override protected lazy val destinationCallRootPath: Path = jesCallPaths.callExecutionRoot - - override protected lazy val destinationJobDetritusPaths: Map[String, Path] = jesCallPaths.detritusPaths - - override val workflowDescriptor: BackendWorkflowDescriptor = jobDescriptor.workflowDescriptor - - override lazy val configurationDescriptor: BackendConfigurationDescriptor = jesConfiguration.configurationDescriptor -} - -object JesCacheHitCopyingActor { - - def props(jobDescriptor: BackendJobDescriptor, - jesConfiguration: JesConfiguration, - initializationData: JesBackendInitializationData, - serviceRegistryActor: ActorRef): Props = { - Props(new JesCacheHitCopyingActor(jobDescriptor, jesConfiguration, initializationData, serviceRegistryActor)).withDispatcher(BackendDispatcher) - } } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesFinalizationActor.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesFinalizationActor.scala index f8dbf0953..0cf19bc2d 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesFinalizationActor.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesFinalizationActor.scala @@ -2,12 +2,11 @@ package cromwell.backend.impl.jes import java.nio.file.Path -import akka.actor.Props import better.files._ import cats.instances.future._ import cats.syntax.functor._ -import cromwell.core.Dispatcher.BackendDispatcher -import cromwell.backend.{BackendWorkflowDescriptor, BackendWorkflowFinalizationActor, JobExecutionMap} +import cromwell.backend._ +import cromwell.backend.standard.{StandardFinalizationActor, StandardFinalizationActorParams} import cromwell.core.CallOutputs import cromwell.core.Dispatcher.IoDispatcher import cromwell.core.path.PathCopier @@ -16,27 +15,28 @@ import wdl4s.TaskCall import scala.concurrent.Future import scala.language.postfixOps -object JesFinalizationActor { - def props(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], jesConfiguration: JesConfiguration, - jobExecutionMap: JobExecutionMap, workflowOutputs: CallOutputs, initializationData: Option[JesBackendInitializationData]) = { - Props(new JesFinalizationActor(workflowDescriptor, - calls, - jesConfiguration, - jobExecutionMap, - workflowOutputs, - initializationData)).withDispatcher(BackendDispatcher) - } +case class JesFinalizationActorParams +( + workflowDescriptor: BackendWorkflowDescriptor, + calls: Set[TaskCall], + jesConfiguration: JesConfiguration, + jobExecutionMap: JobExecutionMap, + workflowOutputs: CallOutputs, + initializationDataOption: Option[BackendInitializationData] +) extends StandardFinalizationActorParams { + override val configurationDescriptor: BackendConfigurationDescriptor = jesConfiguration.configurationDescriptor } -class JesFinalizationActor (override val workflowDescriptor: BackendWorkflowDescriptor, - override val calls: Set[TaskCall], - jesConfiguration: JesConfiguration, jobExecutionMap: JobExecutionMap, - workflowOutputs: CallOutputs, - initializationData: Option[JesBackendInitializationData]) extends BackendWorkflowFinalizationActor { +class JesFinalizationActor(jesParams: JesFinalizationActorParams) + extends StandardFinalizationActor { + + override val standardParams: StandardFinalizationActorParams = jesParams - override val configurationDescriptor = jesConfiguration.configurationDescriptor + lazy val jesConfiguration: JesConfiguration = jesParams.jesConfiguration - private val workflowPaths = initializationData.map { _.workflowPaths } + private val workflowPaths = initializationDataOption.map { + _.asInstanceOf[JesBackendInitializationData].workflowPaths + } private val iOExecutionContext = context.system.dispatchers.lookup(IoDispatcher) @@ -77,7 +77,7 @@ class JesFinalizationActor (override val workflowDescriptor: BackendWorkflowDesc private lazy val logPaths: Seq[Path] = { val allCallPaths = jobExecutionMap flatMap { case (backendJobDescriptor, keys) => - keys map { JesWorkflowPaths(backendJobDescriptor, jesConfiguration)(context.system).toJobPaths(_) } + keys map { JesJobPaths(_, backendJobDescriptor, jesConfiguration)(context.system) } } allCallPaths.toSeq flatMap { callPaths => diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala index 685d2aec3..20d49fa9a 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala @@ -6,55 +6,48 @@ import akka.actor.{ActorRef, Props} import cats.instances.future._ import cats.syntax.functor._ import com.google.api.services.genomics.Genomics -import cromwell.core.Dispatcher.BackendDispatcher -import cromwell.backend.impl.jes.JesInitializationActor._ import cromwell.backend.impl.jes.authentication.{GcsLocalizing, JesAuthInformation} import cromwell.backend.impl.jes.io._ -import cromwell.backend.validation.RuntimeAttributesDefault -import cromwell.backend.validation.RuntimeAttributesKeys._ -import cromwell.backend.{BackendInitializationData, BackendWorkflowDescriptor, BackendWorkflowInitializationActor} -import cromwell.core.WorkflowOptions +import cromwell.backend.standard.{StandardInitializationActor, StandardInitializationActorParams, StandardValidatedRuntimeAttributesBuilder} +import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, BackendWorkflowDescriptor} +import cromwell.core.Dispatcher.BackendDispatcher import cromwell.filesystems.gcs.auth.{ClientSecrets, GoogleAuthMode} import spray.json.JsObject import wdl4s.TaskCall -import wdl4s.types.{WdlBooleanType, WdlFloatType, WdlIntegerType, WdlStringType} -import wdl4s.values.WdlValue import scala.concurrent.Future import scala.language.postfixOps -import scala.util.Try object JesInitializationActor { - val SupportedKeys = Set(CpuKey, MemoryKey, DockerKey, FailOnStderrKey, ContinueOnReturnCodeKey, JesRuntimeAttributes.ZonesKey, - JesRuntimeAttributes.PreemptibleKey, JesRuntimeAttributes.BootDiskSizeKey, JesRuntimeAttributes.DisksKey) - + /* NOTE: Only used by tests */ def props(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], jesConfiguration: JesConfiguration, - serviceRegistryActor: ActorRef): Props = - Props(new JesInitializationActor(workflowDescriptor, calls, jesConfiguration, serviceRegistryActor: ActorRef)).withDispatcher(BackendDispatcher) + serviceRegistryActor: ActorRef): Props = { + val params = JesInitializationActorParams(workflowDescriptor, calls, jesConfiguration, serviceRegistryActor) + Props(new JesInitializationActor(params)).withDispatcher(BackendDispatcher) + } } -class JesInitializationActor(override val workflowDescriptor: BackendWorkflowDescriptor, - override val calls: Set[TaskCall], - private[jes] val jesConfiguration: JesConfiguration, - override val serviceRegistryActor: ActorRef) - extends BackendWorkflowInitializationActor { +case class JesInitializationActorParams +( + workflowDescriptor: BackendWorkflowDescriptor, + calls: Set[TaskCall], + jesConfiguration: JesConfiguration, + serviceRegistryActor: ActorRef +) extends StandardInitializationActorParams { + override val configurationDescriptor: BackendConfigurationDescriptor = jesConfiguration.configurationDescriptor +} + +class JesInitializationActor(jesParams: JesInitializationActorParams) + extends StandardInitializationActor { - override protected def runtimeAttributeValidators: Map[String, (Option[WdlValue]) => Boolean] = Map( - CpuKey -> wdlTypePredicate(valueRequired = false, WdlIntegerType.isCoerceableFrom), - MemoryKey -> wdlTypePredicate(valueRequired = false, WdlStringType.isCoerceableFrom), - DockerKey -> wdlTypePredicate(valueRequired = true, WdlStringType.isCoerceableFrom), - FailOnStderrKey -> wdlTypePredicate(valueRequired = false, WdlBooleanType.isCoerceableFrom), - ContinueOnReturnCodeKey -> continueOnReturnCodePredicate(valueRequired = false), - JesRuntimeAttributes.PreemptibleKey -> wdlTypePredicate(valueRequired = false, WdlIntegerType.isCoerceableFrom), - JesRuntimeAttributes.BootDiskSizeKey -> wdlTypePredicate(valueRequired = false, WdlFloatType.isCoerceableFrom), + override val standardParams: StandardInitializationActorParams = jesParams - // TODO (eventually): make these more appropriate pre-checks - JesRuntimeAttributes.ZonesKey -> wdlTypePredicate(valueRequired = false, WdlStringType.isCoerceableFrom), - JesRuntimeAttributes.DisksKey -> wdlTypePredicate(valueRequired = false, WdlStringType.isCoerceableFrom)) + override lazy val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder = + JesRuntimeAttributes.runtimeAttributesBuilder - override val configurationDescriptor = jesConfiguration.configurationDescriptor + private val jesConfiguration = jesParams.jesConfiguration private[jes] lazy val refreshTokenAuth: Option[JesAuthInformation] = { for { @@ -63,10 +56,6 @@ class JesInitializationActor(override val workflowDescriptor: BackendWorkflowDes } yield GcsLocalizing(clientSecrets, token) } - override protected def coerceDefaultRuntimeAttributes(options: WorkflowOptions): Try[Map[String, WdlValue]] = { - RuntimeAttributesDefault.workflowOptionsDefault(options, JesRuntimeAttributes.coercionMap) - } - /** * A call which happens before anything else runs */ @@ -81,7 +70,7 @@ class JesInitializationActor(override val workflowDescriptor: BackendWorkflowDes workflowPaths = new JesWorkflowPaths(workflowDescriptor, jesConfiguration)(context.system) _ <- if (jesConfiguration.needAuthFileUpload) writeAuthenticationFile(workflowPaths) else Future.successful(()) _ = publishWorkflowRoot(workflowPaths.workflowRoot.toString) - } yield Option(JesBackendInitializationData(workflowPaths, genomics)) + } yield Option(JesBackendInitializationData(workflowPaths, jesConfiguration, genomics)) } private def writeAuthenticationFile(workflowPath: JesWorkflowPaths): Future[Unit] = { @@ -94,7 +83,7 @@ class JesInitializationActor(override val workflowDescriptor: BackendWorkflowDes } getOrElse Future.successful(()) } - def generateAuthJson(authInformation: Option[JesAuthInformation]*) = { + def generateAuthJson(authInformation: Option[JesAuthInformation]*): Option[String] = { authInformation.flatten map { _.toMap } match { case Nil => None case jsons => @@ -102,21 +91,4 @@ class JesInitializationActor(override val workflowDescriptor: BackendWorkflowDes Option(JsObject("auths" -> JsObject(authsValues)).prettyPrint) } } - - /** - * Validate that this WorkflowBackendActor can run all of the calls that it's been assigned - */ - override def validate(): Future[Unit] = { - Future { - calls foreach { call => - val runtimeAttributes = call.task.runtimeAttributes.attrs - val notSupportedAttributes = runtimeAttributes filterKeys { !SupportedKeys.contains(_) } - - if (notSupportedAttributes.nonEmpty) { - val notSupportedAttrString = notSupportedAttributes.keys mkString ", " - workflowLogger.warn(s"Key/s [$notSupportedAttrString] is/are not supported by JesBackend. Unsupported attributes will not be part of jobs executions.") - } - } - } - } } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobCachingActorHelper.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobCachingActorHelper.scala index f24551d71..ad699f36b 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobCachingActorHelper.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobCachingActorHelper.scala @@ -2,17 +2,14 @@ package cromwell.backend.impl.jes import java.nio.file.Path -import akka.actor.{Actor, ActorRef} +import akka.actor.Actor import better.files._ -import cromwell.backend.BackendWorkflowDescriptor -import cromwell.backend.callcaching.JobCachingActorHelper import cromwell.backend.impl.jes.io.{JesAttachedDisk, JesWorkingDisk} +import cromwell.backend.standard.StandardCachingActorHelper import cromwell.core.logging.JobLogging import cromwell.core.path.PathImplicits._ -import scala.util.Try - -trait JesJobCachingActorHelper extends JobCachingActorHelper { +trait JesJobCachingActorHelper extends StandardCachingActorHelper { this: Actor with JobLogging => val ExecParamName = "exec" @@ -21,27 +18,15 @@ trait JesJobCachingActorHelper extends JobCachingActorHelper { val JesMonitoringScript: Path = JesWorkingDisk.MountPoint.resolve("monitoring.sh") val JesMonitoringLogFile: Path = JesWorkingDisk.MountPoint.resolve("monitoring.log") - def jesConfiguration: JesConfiguration - - def initializationData: JesBackendInitializationData - - def serviceRegistryActor: ActorRef - - def workflowDescriptor: BackendWorkflowDescriptor + lazy val initializationData: JesBackendInitializationData = { + backendInitializationDataAs[JesBackendInitializationData] + } - def getPath(str: String): Try[Path] = jesCallPaths.getPath(str) + lazy val jesConfiguration: JesConfiguration = initializationData.jesConfiguration - lazy val jesCallPaths: JesJobPaths = { - val workflowPaths = if (workflowDescriptor.breadCrumbs.isEmpty) { - initializationData.workflowPaths - } else { - new JesWorkflowPaths(workflowDescriptor, jesConfiguration)(context.system) - } - - workflowPaths.toJobPaths(jobDescriptor.key) - } + lazy val jesCallPaths: JesJobPaths = jobPaths.asInstanceOf[JesJobPaths] - lazy val runtimeAttributes = JesRuntimeAttributes(jobDescriptor.runtimeAttributes, jobLogger) + lazy val runtimeAttributes = JesRuntimeAttributes(validatedRuntimeAttributes) lazy val retryable: Boolean = jobDescriptor.key.attempt <= runtimeAttributes.preemptible lazy val workingDisk: JesAttachedDisk = runtimeAttributes.disks.find(_.name == JesWorkingDisk.Name).get @@ -68,20 +53,12 @@ trait JesJobCachingActorHelper extends JobCachingActorHelper { defaultMonitoringOutputPath.toString, File(JesMonitoringLogFile).path, workingDisk) } - // Implements CacheHitDuplicating.startMetadataKeyValues - def startMetadataKeyValues: Map[String, Any] = { - val runtimeAttributesMetadata: Map[String, Any] = runtimeAttributes.asMap map { - case (key, value) => s"runtimeAttributes:$key" -> value - } - - val otherMetadata: Map[String, Any] = Map( + override protected def nonStandardMetadata: Map[String, Any] = { + Map( JesMetadataKeys.GoogleProject -> jesAttributes.project, JesMetadataKeys.ExecutionBucket -> jesAttributes.executionBucket, JesMetadataKeys.EndpointUrl -> jesAttributes.endpointUrl, - "preemptible" -> preemptible, - "cache:allowResultReuse" -> true + "preemptible" -> preemptible ) - - runtimeAttributesMetadata ++ jesCallPaths.metadataPaths ++ otherMetadata } } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobExecutionActor.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobExecutionActor.scala deleted file mode 100644 index 44d85cbfd..000000000 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesJobExecutionActor.scala +++ /dev/null @@ -1,61 +0,0 @@ -package cromwell.backend.impl.jes - -import akka.actor.{ActorRef, Props} -import cromwell.backend._ -import cromwell.backend.standard.{StandardAsyncExecutionActor, StandardSyncExecutionActor, StandardSyncExecutionActorParams} -import cromwell.core.Dispatcher.BackendDispatcher - -/** A default implementation of the sync params. */ -case class JesSyncExecutionActorParams -( - override val jobDescriptor: BackendJobDescriptor, - jesConfiguration: JesConfiguration, - jesBackendInitializationData: JesBackendInitializationData, - override val serviceRegistryActor: ActorRef, - jesBackendSingletonActorOption: Option[ActorRef] -) extends StandardSyncExecutionActorParams { - override val jobIdKey: String = JesJobExecutionActor.JesOperationIdKey - override val asyncJobExecutionActorClass: Class[_ <: StandardAsyncExecutionActor] = classOf[Nothing] - override val configurationDescriptor: BackendConfigurationDescriptor = jesConfiguration.configurationDescriptor - override val backendInitializationDataOption: Option[BackendInitializationData] = Option(jesBackendInitializationData) -} - -object JesJobExecutionActor { - def props(jobDescriptor: BackendJobDescriptor, - jesWorkflowInfo: JesConfiguration, - initializationData: JesBackendInitializationData, - serviceRegistryActor: ActorRef, - jesBackendSingletonActor: Option[ActorRef]): Props = { - val params = JesSyncExecutionActorParams( - jobDescriptor, - jesWorkflowInfo, - initializationData, - serviceRegistryActor, - jesBackendSingletonActor) - Props(new JesJobExecutionActor(params)).withDispatcher(BackendDispatcher) - } - - val JesOperationIdKey = "__jes_operation_id" -} - -case class JesJobExecutionActor(jesParams: JesSyncExecutionActorParams) - extends StandardSyncExecutionActor(jesParams) { - - override def createAsyncRefName(): String = "JesAsyncBackendJobExecutionActor" - - override def createAsyncProps(): Props = jabjeaProps - - private[jes] def jabjeaProps = { - Props( - new JesAsyncBackendJobExecutionActor( - JesAsyncExecutionActorParams( - jesParams.jobDescriptor, - jesParams.jesConfiguration, - jesParams.jesBackendInitializationData, - jesParams.serviceRegistryActor, - jesParams.jesBackendSingletonActorOption, - completionPromise) - ) - ) - } -} diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesPipelineInfo.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesPipelineInfo.scala index 063a374b8..990ee7c98 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesPipelineInfo.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesPipelineInfo.scala @@ -31,13 +31,7 @@ trait JesPipelineInfoBuilder { object NonPreemptibleJesPipelineInfoBuilder extends JesPipelineInfoBuilder { def build(commandLine: String, runtimeAttributes: JesRuntimeAttributes): JesPipelineInfo = { - /* - It should be impossible for docker to be None here. Enforcing that w/ ADTs seemed more trouble than - it was worth. If you're ever debugging a NoSuchElementException which leads you here, that means - the more trouble than worth calculation was incorrect and we should have separate RuntimeAttributes for - docker and no docker cases - */ - val dockerImage = runtimeAttributes.dockerImage.get + val dockerImage = runtimeAttributes.dockerImage val resources = buildResources(runtimeAttributes).setPreemptible(false) new NonPreemptibleJesPipelineInfoBuilder(resources, buildDockerExecutor(commandLine, dockerImage)) } @@ -45,8 +39,7 @@ object NonPreemptibleJesPipelineInfoBuilder extends JesPipelineInfoBuilder { object PreemptibleJesPipelineInfoBuilder extends JesPipelineInfoBuilder { def build(commandLine: String, runtimeAttributes: JesRuntimeAttributes): JesPipelineInfo = { - // See comment above - val dockerImage = runtimeAttributes.dockerImage.get + val dockerImage = runtimeAttributes.dockerImage val resources = buildResources(runtimeAttributes).setPreemptible(true) new PreemptibleJesPipelineInfoBuilder(resources, buildDockerExecutor(commandLine, dockerImage)) } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala index 374ce05b9..a878974a8 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala @@ -5,11 +5,8 @@ import cats.syntax.cartesian._ import cats.syntax.validated._ import cromwell.backend.MemorySize import cromwell.backend.impl.jes.io.{JesAttachedDisk, JesWorkingDisk} -import cromwell.backend.validation.RuntimeAttributesDefault._ -import cromwell.backend.validation.RuntimeAttributesKeys._ -import cromwell.backend.validation.RuntimeAttributesValidation._ -import cromwell.backend.validation._ -import lenthall.exception.MessageAggregation +import cromwell.backend.standard.StandardValidatedRuntimeAttributesBuilder +import cromwell.backend.validation.{BooleanRuntimeAttributesValidation, _} import lenthall.validation.ErrorOr._ import org.slf4j.Logger import wdl4s.types._ @@ -21,28 +18,12 @@ case class JesRuntimeAttributes(cpu: Int, bootDiskSize: Int, memory: MemorySize, disks: Seq[JesAttachedDisk], - dockerImage: Option[String], + dockerImage: String, failOnStderr: Boolean, continueOnReturnCode: ContinueOnReturnCode, - noAddress: Boolean) { - import JesRuntimeAttributes._ - - lazy val asMap = Map[String, Any]( - CpuKey -> cpu.toString, - ZonesKey -> zones.mkString(","), - PreemptibleKey -> preemptible.toString, - BootDiskSizeKey -> bootDiskSize.toString, - MemoryKey -> memory.toString, - DisksKey -> disks.mkString(","), - DockerKey -> dockerImage.get, - FailOnStderrKey -> failOnStderr.toString, - ContinueOnReturnCodeKey -> continueOnReturnCode - ) -} + noAddress: Boolean) object JesRuntimeAttributes { - private val CpuDefaultValue = 1 - private val ContinueOnReturnCodeDefaultValue = 0 private val MemoryDefaultValue = "2 GB" val ZonesKey = "zones" @@ -60,109 +41,133 @@ object JesRuntimeAttributes { val DisksKey = "disks" private val DisksDefaultValue = s"${JesWorkingDisk.Name} 10 SSD" - val staticDefaults = Map( - CpuKey -> WdlInteger(CpuDefaultValue), - DisksKey -> WdlString(DisksDefaultValue), - ZonesKey -> WdlString(ZoneDefaultValue), - ContinueOnReturnCodeKey -> WdlInteger(ContinueOnReturnCodeDefaultValue), - FailOnStderrKey -> WdlBoolean.False, - PreemptibleKey -> WdlInteger(PreemptibleDefaultValue), - MemoryKey -> WdlString(MemoryDefaultValue), - BootDiskSizeKey -> WdlInteger(BootDiskSizeDefaultValue), - NoAddressKey -> WdlBoolean(NoAddressDefaultValue) - ) - - private[jes] val coercionMap: Map[String, Set[WdlType]] = Map( - CpuKey -> Set(WdlIntegerType), - DisksKey -> Set(WdlStringType, WdlArrayType(WdlStringType)), - ZonesKey -> Set(WdlStringType, WdlArrayType(WdlStringType)), - ContinueOnReturnCodeKey -> ContinueOnReturnCode.validWdlTypes, - FailOnStderrKey -> Set(WdlBooleanType), - PreemptibleKey -> Set(WdlIntegerType), - MemoryKey -> Set(WdlStringType), - BootDiskSizeKey -> Set(WdlIntegerType), - NoAddressKey -> Set(WdlBooleanType), - DockerKey -> Set(WdlStringType) - ) - - def apply(attrs: Map[String, WdlValue], logger: Logger): JesRuntimeAttributes = { - warnUnrecognized(attrs.keySet, coercionMap.keySet, logger) - - val cpu = validateCpu(attrs.get(CpuKey), noValueFoundFor(CpuKey)) - val memory = validateMemory(attrs.get(MemoryKey), noValueFoundFor(MemoryKey)) - val docker = validateDocker(attrs.get(DockerKey), noValueFoundFor(DockerKey)) - val failOnStderr = validateFailOnStderr(attrs.get(FailOnStderrKey), noValueFoundFor(FailOnStderrKey)) - val continueOnReturnCode = validateContinueOnReturnCode(attrs.get(ContinueOnReturnCodeKey), noValueFoundFor(ContinueOnReturnCodeKey)) - - val zones = validateZone(attrs(ZonesKey)) - val preemptible = validatePreemptible(attrs(PreemptibleKey)) - val noAddress = validateNoAddress(attrs(NoAddressKey)) - val bootDiskSize = validateBootDisk(attrs(BootDiskSizeKey)) - val disks = validateLocalDisks(attrs(DisksKey)) - (cpu |@| zones |@| preemptible |@| bootDiskSize |@| memory |@| disks |@| docker |@| failOnStderr |@| continueOnReturnCode |@| noAddress) map { - new JesRuntimeAttributes(_, _, _, _, _, _, _, _, _, _) - } match { - case Valid(x) => x - case Invalid(nel) => throw new RuntimeException with MessageAggregation { - override def exceptionContext: String = "Runtime attribute validation failed" - override def errorMessages: Traversable[String] = nel.toList - } - } + private val cpuValidation: RuntimeAttributesValidation[Int] = CpuValidation.default + + private val disksValidation: RuntimeAttributesValidation[Seq[JesAttachedDisk]] = + DisksValidation.withDefault(WdlString(DisksDefaultValue)) + + private val zonesValidation: RuntimeAttributesValidation[Vector[String]] = + ZonesValidation.withDefault(WdlString(ZoneDefaultValue)) + + private val preemptibleValidation: RuntimeAttributesValidation[Int] = + new IntRuntimeAttributesValidation(JesRuntimeAttributes.PreemptibleKey) + .withDefault(WdlInteger(PreemptibleDefaultValue)) + + private val memoryValidation: RuntimeAttributesValidation[MemorySize] = + MemoryValidation.withDefaultMemory(MemorySize.parse(MemoryDefaultValue).get) + + private val bootDiskSizeValidation: RuntimeAttributesValidation[Int] = + new IntRuntimeAttributesValidation(JesRuntimeAttributes.BootDiskSizeKey) + .withDefault(WdlInteger(BootDiskSizeDefaultValue)) + + private val noAddressValidation: RuntimeAttributesValidation[Boolean] = + new BooleanRuntimeAttributesValidation(JesRuntimeAttributes.NoAddressKey) + .withDefault(WdlBoolean(NoAddressDefaultValue)) + + private val dockerValidation: RuntimeAttributesValidation[String] = DockerValidation.instance + + val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder = + StandardValidatedRuntimeAttributesBuilder.default.withValidation( + cpuValidation, + disksValidation, + zonesValidation, + preemptibleValidation, + memoryValidation, + bootDiskSizeValidation, + noAddressValidation, + dockerValidation + ) + + def apply(validatedRuntimeAttributes: ValidatedRuntimeAttributes): JesRuntimeAttributes = { + val cpu: Int = RuntimeAttributesValidation.extract(cpuValidation, validatedRuntimeAttributes) + val zones: Vector[String] = RuntimeAttributesValidation.extract(zonesValidation, validatedRuntimeAttributes) + val preemptible: Int = RuntimeAttributesValidation.extract(preemptibleValidation, validatedRuntimeAttributes) + val bootDiskSize: Int = RuntimeAttributesValidation.extract(bootDiskSizeValidation, validatedRuntimeAttributes) + val memory: MemorySize = RuntimeAttributesValidation.extract(memoryValidation, validatedRuntimeAttributes) + val disks: Seq[JesAttachedDisk] = RuntimeAttributesValidation.extract(disksValidation, validatedRuntimeAttributes) + val docker: String = RuntimeAttributesValidation.extract(dockerValidation, validatedRuntimeAttributes) + val failOnStderr = RuntimeAttributesValidation.extract(FailOnStderrValidation.default, validatedRuntimeAttributes) + val continueOnReturnCode = + RuntimeAttributesValidation.extract(ContinueOnReturnCodeValidation.default, validatedRuntimeAttributes) + val noAddress: Boolean = RuntimeAttributesValidation.extract(noAddressValidation, validatedRuntimeAttributes) + + new JesRuntimeAttributes( + cpu, + zones, + preemptible, + bootDiskSize, + memory, + disks, + docker, + failOnStderr, + continueOnReturnCode, + noAddress + ) } - private def validateZone(zoneValue: WdlValue): ErrorOr[Vector[String]] = { - zoneValue match { - case WdlString(s) => s.split("\\s+").toVector.validNel - case WdlArray(wdlType, value) if wdlType.memberType == WdlStringType => - value.map(_.valueString).toVector.validNel - case _ => s"Expecting $ZonesKey runtime attribute to be either a whitespace separated String or an Array[String]".invalidNel - } + // NOTE: Currently only used by test specs + private[jes] def apply(attrs: Map[String, WdlValue], logger: Logger): JesRuntimeAttributes = { + val runtimeAttributesBuilder = JesRuntimeAttributes.runtimeAttributesBuilder + val validatedRuntimeAttributes = runtimeAttributesBuilder.build(attrs, logger) + apply(validatedRuntimeAttributes) } +} - private def contextualizeFailure[T](validation: ErrorOr[T], key: String): ErrorOr[T] = { - validation.leftMap[String](errors => s"Failed to validate $key runtime attribute: " + errors.toList.mkString(",")).toValidatedNel - } +object ZonesValidation extends RuntimeAttributesValidation[Vector[String]] { + override def key: String = JesRuntimeAttributes.ZonesKey + + override def coercion: Traversable[WdlType] = Set(WdlStringType, WdlArrayType(WdlStringType)) - private def validatePreemptible(preemptible: WdlValue): ErrorOr[Int] = { - contextualizeFailure(validateInt(preemptible), PreemptibleKey) + override protected def validateValue: PartialFunction[WdlValue, ErrorOr[Vector[String]]] = { + case WdlString(s) => s.split("\\s+").toVector.validNel + case WdlArray(wdlType, value) if wdlType.memberType == WdlStringType => + value.map(_.valueString).toVector.validNel } - private def validateNoAddress(noAddress: WdlValue): ErrorOr[Boolean] = { - contextualizeFailure(validateBoolean(noAddress), NoAddressKey) + override protected def missingValueMessage: String = + s"Expecting $key runtime attribute to be either a whitespace separated String or an Array[String]" +} + +object DisksValidation extends RuntimeAttributesValidation[Seq[JesAttachedDisk]] { + override def key: String = JesRuntimeAttributes.DisksKey + + override def coercion: Traversable[WdlType] = Set(WdlStringType, WdlArrayType(WdlStringType)) + + override protected def validateValue: PartialFunction[WdlValue, ErrorOr[Seq[JesAttachedDisk]]] = { + case WdlString(value) => validateLocalDisks(value.split(",\\s*").toSeq) + case WdlArray(wdlType, values) if wdlType.memberType == WdlStringType => + validateLocalDisks(values.map(_.valueString)) } - private def validateBootDisk(diskSize: WdlValue): ErrorOr[Int] = diskSize match { - case x if WdlIntegerType.isCoerceableFrom(x.wdlType) => - WdlIntegerType.coerceRawValue(x) match { - case scala.util.Success(x: WdlInteger) => x.value.intValue.validNel - case scala.util.Success(unhandled) => s"Coercion was expected to create an Integer but instead got $unhandled".invalidNel - case scala.util.Failure(t) => s"Expecting $BootDiskSizeKey runtime attribute to be an Integer".invalidNel - } + private def validateLocalDisks(disks: Seq[String]): ErrorOr[Seq[JesAttachedDisk]] = { + val diskNels: Seq[ErrorOr[JesAttachedDisk]] = disks map validateLocalDisk + val sequenced: ErrorOr[Seq[JesAttachedDisk]] = sequenceNels(diskNels) + val defaulted: ErrorOr[Seq[JesAttachedDisk]] = addDefault(sequenced) + defaulted } - private def validateLocalDisks(value: WdlValue): ErrorOr[Seq[JesAttachedDisk]] = { - val nels: Seq[ErrorOr[JesAttachedDisk]] = value match { - case WdlString(s) => s.split(",\\s*").toSeq.map(validateLocalDisk) - case WdlArray(wdlType, seq) if wdlType.memberType == WdlStringType => - seq.map(_.valueString).map(validateLocalDisk) - case _ => - Seq(s"Expecting $DisksKey runtime attribute to be a comma separated String or Array[String]".invalidNel) + private def validateLocalDisk(disk: String): ErrorOr[JesAttachedDisk] = { + JesAttachedDisk.parse(disk) match { + case scala.util.Success(attachedDisk) => attachedDisk.validNel + case scala.util.Failure(ex) => ex.getMessage.invalidNel } + } + private def sequenceNels(nels: Seq[ErrorOr[JesAttachedDisk]]): ErrorOr[Seq[JesAttachedDisk]] = { val emptyDiskNel = Vector.empty[JesAttachedDisk].validNel[String] - val disksNel = nels.foldLeft(emptyDiskNel)((acc, v) => (acc |@| v) map { (a, v) => a :+ v }) - - disksNel map { - case disks if disks.exists(_.name == JesWorkingDisk.Name) => disks - case disks => disks :+ JesAttachedDisk.parse(DisksDefaultValue).get + val disksNel: ErrorOr[Vector[JesAttachedDisk]] = nels.foldLeft(emptyDiskNel) { + (acc, v) => (acc |@| v) map { (a, v) => a :+ v } } + disksNel } - private def validateLocalDisk(disk: String): ErrorOr[JesAttachedDisk] = { - JesAttachedDisk.parse(disk) match { - case scala.util.Success(localDisk) => localDisk.validNel - case scala.util.Failure(ex) => ex.getMessage.invalidNel + private def addDefault(disksNel: ErrorOr[Seq[JesAttachedDisk]]): ErrorOr[Seq[JesAttachedDisk]] = { + disksNel map { + case disks if disks.exists(_.name == JesWorkingDisk.Name) => disks + case disks => disks :+ JesWorkingDisk.Default } } + override protected def missingValueMessage: String = + s"Expecting $key runtime attribute to be a comma separated String or Array[String]" } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesWorkflowPaths.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesWorkflowPaths.scala index 7feade96d..615525e2d 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesWorkflowPaths.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesWorkflowPaths.scala @@ -13,13 +13,14 @@ import cromwell.core.path.PathImplicits._ import cromwell.filesystems.gcs.{GcsPathBuilderFactory, RetryableGcsPathBuilder} import scala.language.postfixOps +import scala.util.Try object JesWorkflowPaths { private val GcsRootOptionKey = "jes_gcs_root" private val AuthFilePathOptionKey = "auth_bucket" def apply(workflowDescriptor: BackendWorkflowDescriptor, - jesConfiguration: JesConfiguration)(implicit actorSystem: ActorSystem) = { + jesConfiguration: JesConfiguration)(implicit actorSystem: ActorSystem): JesWorkflowPaths = { new JesWorkflowPaths(workflowDescriptor, jesConfiguration) } } @@ -27,11 +28,12 @@ object JesWorkflowPaths { class JesWorkflowPaths(val workflowDescriptor: BackendWorkflowDescriptor, jesConfiguration: JesConfiguration)(implicit actorSystem: ActorSystem) extends WorkflowPaths { - override lazy val executionRootString = workflowDescriptor.workflowOptions.getOrElse(JesWorkflowPaths.GcsRootOptionKey, jesConfiguration.root) + override lazy val executionRootString: String = + workflowDescriptor.workflowOptions.getOrElse(JesWorkflowPaths.GcsRootOptionKey, jesConfiguration.root) private val workflowOptions: WorkflowOptions = workflowDescriptor.workflowOptions val gcsPathBuilder: RetryableGcsPathBuilder = jesConfiguration.gcsPathBuilderFactory.withOptions(workflowOptions) - def getHash(gcsUrl: Path) = gcsPathBuilder.getHash(gcsUrl) + def getHash(gcsUrl: Path): Try[String] = gcsPathBuilder.getHash(gcsUrl) val gcsAuthFilePath: Path = { /* @@ -39,10 +41,10 @@ class JesWorkflowPaths(val workflowDescriptor: BackendWorkflowDescriptor, * unlike everywhere else where the filesystem used is built from gcsFileSystemAuth */ val genomicsCredentials = jesConfiguration.jesAuths.genomics - + // The default auth file bucket is always at the root of the root workflow val defaultBucket = executionRoot.resolve(workflowDescriptor.rootWorkflow.unqualifiedName).resolve(workflowDescriptor.rootWorkflowId.toString) - + val bucket = workflowDescriptor.workflowOptions.get(JesWorkflowPaths.AuthFilePathOptionKey) getOrElse defaultBucket.toRealString val authBucket = GcsPathBuilderFactory(genomicsCredentials).withOptions(workflowOptions).build(bucket) recover { case ex => throw new Exception(s"Invalid gcs auth_bucket path $bucket", ex) @@ -50,14 +52,16 @@ class JesWorkflowPaths(val workflowDescriptor: BackendWorkflowDescriptor, authBucket.resolve(s"${workflowDescriptor.rootWorkflowId}_auth.json") } - - - val monitoringPath = workflowOptions.get(WorkflowOptionKeys.MonitoringScript).toOption map { path => + + val monitoringPath: Option[Path] = workflowOptions.get(WorkflowOptionKeys.MonitoringScript).toOption map { path => // Fail here if the path exists but can't be built getPath(path).get } - override def toJobPaths(jobKey: BackendJobDescriptorKey) = JesJobPaths(jobKey, workflowDescriptor, jesConfiguration) + override def toJobPaths(jobKey: BackendJobDescriptorKey, + jobWorkflowDescriptor: BackendWorkflowDescriptor): JesJobPaths = { + JesJobPaths(jobKey, jobWorkflowDescriptor, jesConfiguration) + } override def config: Config = jesConfiguration.configurationDescriptor.backendConfig override def pathBuilders: List[PathBuilder] = List(gcsPathBuilder) } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/io/JesAttachedDisk.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/io/JesAttachedDisk.scala index 4066c1f41..38e702dc2 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/io/JesAttachedDisk.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/io/JesAttachedDisk.scala @@ -11,14 +11,15 @@ import lenthall.exception.MessageAggregation import wdl4s.values._ import scala.util.Try +import scala.util.matching.Regex object JesAttachedDisk { val Identifier = "[a-zA-Z0-9-_]+" val Directory = """/[^\s]+""" val Integer = "[1-9][0-9]*" - val WorkingDiskPattern = s"""${JesWorkingDisk.Name}\\s+($Integer)\\s+($Identifier)""".r - val MountedDiskPattern = s"""($Directory)\\s+($Integer)\\s+($Identifier)""".r + val WorkingDiskPattern: Regex = s"""${JesWorkingDisk.Name}\\s+($Integer)\\s+($Identifier)""".r + val MountedDiskPattern: Regex = s"""($Directory)\\s+($Integer)\\s+($Identifier)""".r def parse(s: String): Try[JesAttachedDisk] = { val validation: ErrorOr[JesAttachedDisk] = s match { @@ -38,7 +39,7 @@ object JesAttachedDisk { case Invalid(nels) => throw new UnsupportedOperationException with MessageAggregation { val exceptionContext = "" - val errorMessages = nels.toList + val errorMessages: List[String] = nels.toList } }) } @@ -81,8 +82,9 @@ case class JesEmptyMountedDisk(diskType: DiskType, sizeGb: Int, mountPoint: Path } object JesWorkingDisk { - val MountPoint = Paths.get("/cromwell_root") + val MountPoint: Path = Paths.get("/cromwell_root") val Name = "local-disk" + val Default = JesWorkingDisk(DiskType.SSD, 10) } case class JesWorkingDisk(diskType: DiskType, sizeGb: Int) extends JesAttachedDisk { diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala index bbd713343..40fc7f44c 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala @@ -14,21 +14,21 @@ import cromwell.backend.impl.jes.JesAsyncBackendJobExecutionActor.JesPendingExec import cromwell.backend.impl.jes.RunStatus.Failed import cromwell.backend.impl.jes.io.{DiskType, JesWorkingDisk} import cromwell.backend.impl.jes.statuspolling.JesApiQueryManager.DoPoll -import cromwell.backend.standard.StandardAsyncJob +import cromwell.backend.standard.{DefaultStandardAsyncExecutionActorParams, StandardAsyncExecutionActorParams, StandardAsyncJob} +import cromwell.core._ import cromwell.core.logging.JobLogger import cromwell.core.path.PathImplicits._ -import cromwell.core.{WorkflowId, WorkflowOptions, _} -import cromwell.filesystems.gcs.{GcsPathBuilder, GcsPathBuilderFactory} import cromwell.filesystems.gcs.auth.GoogleAuthMode.NoAuthMode +import cromwell.filesystems.gcs.{GcsPathBuilder, GcsPathBuilderFactory} import cromwell.util.SampleWdl import org.scalatest._ import org.scalatest.prop.Tables.Table import org.slf4j.Logger import org.specs2.mock.Mockito import spray.json.{JsObject, JsValue} +import wdl4s._ import wdl4s.types.{WdlArrayType, WdlFileType, WdlMapType, WdlStringType} import wdl4s.values.{WdlArray, WdlFile, WdlMap, WdlString, WdlValue} -import wdl4s.{LocallyQualifiedName, FullyQualifiedName => _, _} import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future, Promise} @@ -76,11 +76,11 @@ class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackend private def buildInitializationData(jobDescriptor: BackendJobDescriptor, configuration: JesConfiguration) = { val workflowPaths = JesWorkflowPaths(jobDescriptor.workflowDescriptor, configuration)(system) - JesBackendInitializationData(workflowPaths, null) + JesBackendInitializationData(workflowPaths, configuration, null) } - class TestableJesJobExecutionActor(jesParams: JesAsyncExecutionActorParams, functions: JesExpressionFunctions) - extends JesAsyncBackendJobExecutionActor(jesParams) { + class TestableJesJobExecutionActor(params: StandardAsyncExecutionActorParams, functions: JesExpressionFunctions) + extends JesAsyncBackendJobExecutionActor(params) { def this(jobDescriptor: BackendJobDescriptor, promise: Promise[BackendJobExecutionResponse], @@ -88,11 +88,12 @@ class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackend functions: JesExpressionFunctions = TestableJesExpressionFunctions, jesSingletonActor: ActorRef = emptyActor) = { this( - JesAsyncExecutionActorParams( - jobDescriptor, - jesConfiguration, - buildInitializationData(jobDescriptor, jesConfiguration), + DefaultStandardAsyncExecutionActorParams( + JesAsyncBackendJobExecutionActor.JesOperationIdKey, emptyActor, + jobDescriptor, + jesConfiguration.configurationDescriptor, + Option(buildInitializationData(jobDescriptor, jesConfiguration)), Option(jesSingletonActor), promise ), diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala index 73b8cb6de..44688edd8 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala @@ -22,11 +22,11 @@ import scala.concurrent.duration._ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpec") with FlatSpecLike with Matchers with ImplicitSender with Mockito { - val Timeout = 5.second.dilated + val Timeout: FiniteDuration = 5.second.dilated import BackendSpec._ - val HelloWorld = + val HelloWorld: String = s""" |task hello { | String addressee = "you" @@ -45,7 +45,7 @@ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpe |} """.stripMargin - val globalConfig = ConfigFactory.parseString( + val globalConfig: Config = ConfigFactory.parseString( """ |google { | @@ -66,7 +66,7 @@ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpe |} | """.stripMargin) - val backendConfigTemplate = + val backendConfigTemplate: String = """ | // Google project | project = "my-cromwell-workflows" @@ -96,7 +96,7 @@ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpe |[DOCKERHUBCONFIG] |""".stripMargin - val refreshTokenConfigTemplate = + val refreshTokenConfigTemplate: String = """ | // Google project | project = "my-cromwell-workflows" @@ -124,9 +124,9 @@ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpe | } |""".stripMargin - val backendConfig = ConfigFactory.parseString(backendConfigTemplate.replace("[DOCKERHUBCONFIG]", "")) + val backendConfig: Config = ConfigFactory.parseString(backendConfigTemplate.replace("[DOCKERHUBCONFIG]", "")) - val dockerBackendConfig = ConfigFactory.parseString(backendConfigTemplate.replace("[DOCKERHUBCONFIG]", + val dockerBackendConfig: Config = ConfigFactory.parseString(backendConfigTemplate.replace("[DOCKERHUBCONFIG]", """ |dockerhub { | account = "my@docker.account" @@ -136,7 +136,7 @@ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpe val defaultBackendConfig = BackendConfigurationDescriptor(backendConfig, globalConfig) - val refreshTokenConfig = ConfigFactory.parseString(refreshTokenConfigTemplate) + val refreshTokenConfig: Config = ConfigFactory.parseString(refreshTokenConfigTemplate) private def getJesBackend(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], conf: BackendConfigurationDescriptor) = { system.actorOf(JesInitializationActor.props(workflowDescriptor, calls, new JesConfiguration(conf), emptyActor)) @@ -151,7 +151,7 @@ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpe val backend = getJesBackend(workflowDescriptor, workflowDescriptor.workflow.taskCalls, defaultBackendConfig) val eventPattern = - "Key/s [test] is/are not supported by JesBackend. Unsupported attributes will not be part of jobs executions." + "Key/s [test] is/are not supported by backend. Unsupported attributes will not be part of job executions." EventFilter.warning(pattern = escapePattern(eventPattern), occurrences = 1) intercept { backend ! Initialize } diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesJobExecutionActorSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesJobExecutionActorSpec.scala index b12703c23..5b72db08b 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesJobExecutionActorSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesJobExecutionActorSpec.scala @@ -1,17 +1,16 @@ package cromwell.backend.impl.jes import akka.actor.{Actor, ActorRef, Props} -import akka.testkit.{TestActorRef, TestProbe} +import akka.testkit._ import cromwell.backend.BackendJobDescriptor +import cromwell.backend.BackendJobExecutionActor.{ExecuteJobCommand, JobFailedNonRetryableResponse} +import cromwell.backend.impl.jes.ControllableFailingJabjea.JabjeaExplode +import cromwell.backend.standard.{DefaultStandardSyncExecutionActorParams, StandardSyncExecutionActor, StandardSyncExecutionActorParams} import cromwell.core.TestKitSuite import org.scalatest.{FlatSpecLike, Matchers} import org.specs2.mock.Mockito import scala.concurrent.duration._ -import akka.testkit._ -import cromwell.backend.BackendJobExecutionActor.{ExecuteJobCommand, JobFailedNonRetryableResponse} -import cromwell.backend.impl.jes.ControllableFailingJabjea.JabjeaExplode - import scala.concurrent.{ExecutionContext, Promise} class JesJobExecutionActorSpec extends TestKitSuite("JesJobExecutionActorSpec") with FlatSpecLike with Matchers with Mockito { @@ -29,10 +28,13 @@ class JesJobExecutionActorSpec extends TestKitSuite("JesJobExecutionActorSpec") val serviceRegistryActor = system.actorOf(Props.empty) val jesBackendSingletonActor = Option(system.actorOf(Props.empty)) + initializationData.jesConfiguration returns jesWorkflowInfo + val parent = TestProbe() val deathwatch = TestProbe() - val params = JesSyncExecutionActorParams(jobDescriptor, jesWorkflowInfo, initializationData, serviceRegistryActor, - jesBackendSingletonActor) + val params = DefaultStandardSyncExecutionActorParams(JesAsyncBackendJobExecutionActor.JesOperationIdKey, serviceRegistryActor, + jobDescriptor, null, Option(initializationData), jesBackendSingletonActor, + classOf[JesAsyncBackendJobExecutionActor]) val testJJEA = TestActorRef[TestJesJobExecutionActor]( props = Props(new TestJesJobExecutionActor(params, Props(new ConstructorFailingJABJEA))), supervisor = parent.ref) @@ -57,11 +59,14 @@ class JesJobExecutionActorSpec extends TestKitSuite("JesJobExecutionActorSpec") val serviceRegistryActor = system.actorOf(Props.empty) val jesBackendSingletonActor = Option(system.actorOf(Props.empty)) + initializationData.jesConfiguration returns jesWorkflowInfo + val parent = TestProbe() val deathwatch = TestProbe() val jabjeaConstructionPromise = Promise[ActorRef]() - val params = JesSyncExecutionActorParams(jobDescriptor, jesWorkflowInfo, initializationData, serviceRegistryActor, - jesBackendSingletonActor) + val params = DefaultStandardSyncExecutionActorParams(JesAsyncBackendJobExecutionActor.JesOperationIdKey, serviceRegistryActor, + jobDescriptor, null, Option(initializationData), jesBackendSingletonActor, + classOf[JesAsyncBackendJobExecutionActor]) val testJJEA = TestActorRef[TestJesJobExecutionActor]( props = Props(new TestJesJobExecutionActor(params, Props(new ControllableFailingJabjea(jabjeaConstructionPromise)))), supervisor = parent.ref) @@ -85,9 +90,9 @@ class JesJobExecutionActorSpec extends TestKitSuite("JesJobExecutionActorSpec") } } -class TestJesJobExecutionActor(jesParams: JesSyncExecutionActorParams, - fakeJabjeaProps: Props) extends JesJobExecutionActor(jesParams) { - override def jabjeaProps: Props = fakeJabjeaProps +class TestJesJobExecutionActor(params: StandardSyncExecutionActorParams, + fakeJabjeaProps: Props) extends StandardSyncExecutionActor(params) { + override def createAsyncProps(): Props = fakeJabjeaProps } class ConstructorFailingJABJEA extends ControllableFailingJabjea(Promise[ActorRef]()) { diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala index 204907110..43b62287c 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala @@ -14,14 +14,14 @@ import wdl4s.values.{WdlArray, WdlBoolean, WdlInteger, WdlString, WdlValue} class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { - def workflowOptionsWithDefaultRA(defaults: Map[String, JsValue]) = { + def workflowOptionsWithDefaultRA(defaults: Map[String, JsValue]): WorkflowOptions = { WorkflowOptions(JsObject(Map( "default_runtime_attributes" -> JsObject(defaults) ))) } - val expectedDefaults = new JesRuntimeAttributes(1, Vector("us-central1-b"), 0, 10, MemorySize(2, MemoryUnit.GB), Seq(JesWorkingDisk(DiskType.SSD, 10)), None, false, ContinueOnReturnCodeSet(Set(0)), false) - val expectedDefaultsPlusUbuntuDocker = expectedDefaults.copy(dockerImage = Some("ubuntu:latest")) + val expectedDefaults = new JesRuntimeAttributes(1, Vector("us-central1-b"), 0, 10, MemorySize(2, MemoryUnit.GB), + Seq(JesWorkingDisk(DiskType.SSD, 10)), "ubuntu:latest", false, ContinueOnReturnCodeSet(Set(0)), false) "JesRuntimeAttributes" should { @@ -32,7 +32,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid Docker entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest")) - val expectedRuntimeAttributes = expectedDefaults.copy(dockerImage = Option("ubuntu:latest")) + val expectedRuntimeAttributes = expectedDefaults assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -43,7 +43,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid failOnStderr entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "failOnStderr" -> WdlBoolean(true)) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(failOnStderr = true) + val expectedRuntimeAttributes = expectedDefaults.copy(failOnStderr = true) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -54,19 +54,19 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid continueOnReturnCode entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "continueOnReturnCode" -> WdlInteger(1)) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(continueOnReturnCode = ContinueOnReturnCodeSet(Set(1))) + val expectedRuntimeAttributes = expectedDefaults.copy(continueOnReturnCode = ContinueOnReturnCodeSet(Set(1))) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } "validate a valid continueOnReturnCode array entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "continueOnReturnCode" -> WdlArray(WdlArrayType(WdlIntegerType), Array(WdlInteger(1), WdlInteger(2)))) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(continueOnReturnCode = ContinueOnReturnCodeSet(Set(1, 2))) + val expectedRuntimeAttributes = expectedDefaults.copy(continueOnReturnCode = ContinueOnReturnCodeSet(Set(1, 2))) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } "coerce then validate a valid continueOnReturnCode array entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "continueOnReturnCode" -> WdlArray(WdlArrayType(WdlStringType), Array(WdlString("1"), WdlString("2")))) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(continueOnReturnCode = ContinueOnReturnCodeSet(Set(1, 2))) + val expectedRuntimeAttributes = expectedDefaults.copy(continueOnReturnCode = ContinueOnReturnCodeSet(Set(1, 2))) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -77,13 +77,13 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid cpu entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "cpu" -> WdlInteger(2)) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(cpu = 2) + val expectedRuntimeAttributes = expectedDefaults.copy(cpu = 2) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } "validate a valid cpu string entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "cpu" -> WdlString("2")) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(cpu = 2) + val expectedRuntimeAttributes = expectedDefaults.copy(cpu = 2) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -94,7 +94,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid zones entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "zones" -> WdlString("us-central-z")) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(zones = Vector("us-central-z")) + val expectedRuntimeAttributes = expectedDefaults.copy(zones = Vector("us-central-z")) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -105,7 +105,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid array zones entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "zones" -> WdlArray(WdlArrayType(WdlStringType), Array(WdlString("us-central1-y"), WdlString("us-central1-z")))) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(zones = Vector("us-central1-y", "us-central1-z")) + val expectedRuntimeAttributes = expectedDefaults.copy(zones = Vector("us-central1-y", "us-central1-z")) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -116,18 +116,19 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid preemptible entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "preemptible" -> WdlInteger(3)) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(preemptible = 3) + val expectedRuntimeAttributes = expectedDefaults.copy(preemptible = 3) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } "fail to validate an invalid preemptible entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "preemptible" -> WdlString("value")) - assertJesRuntimeAttributesFailedCreation(runtimeAttributes, "Failed to validate preemptible runtime attribute: Could not coerce value into an integer") + assertJesRuntimeAttributesFailedCreation(runtimeAttributes, + "Expecting preemptible runtime attribute to be an Integer") } "validate a valid bootDiskSizeGb entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "bootDiskSizeGb" -> WdlInteger(4)) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(bootDiskSize = 4) + val expectedRuntimeAttributes = expectedDefaults.copy(bootDiskSize = 4) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -138,7 +139,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid disks entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "disks" -> WdlString("local-disk 20 SSD")) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(disks = Seq(JesAttachedDisk.parse("local-disk 20 SSD").get)) + val expectedRuntimeAttributes = expectedDefaults.copy(disks = Seq(JesAttachedDisk.parse("local-disk 20 SSD").get)) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -149,7 +150,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid disks array entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "disks" -> WdlArray(WdlArrayType(WdlStringType), Array(WdlString("local-disk 20 SSD"), WdlString("local-disk 30 SSD")))) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(disks = Seq(JesAttachedDisk.parse("local-disk 20 SSD").get, JesAttachedDisk.parse("local-disk 30 SSD").get)) + val expectedRuntimeAttributes = expectedDefaults.copy(disks = Seq(JesAttachedDisk.parse("local-disk 20 SSD").get, JesAttachedDisk.parse("local-disk 30 SSD").get)) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -160,7 +161,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid memory entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "memory" -> WdlString("1 GB")) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(memory = MemorySize.parse("1 GB").get) + val expectedRuntimeAttributes = expectedDefaults.copy(memory = MemorySize.parse("1 GB").get) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } @@ -171,18 +172,19 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { "validate a valid noAddress entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "noAddress" -> WdlBoolean(true)) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker.copy(noAddress = true) + val expectedRuntimeAttributes = expectedDefaults.copy(noAddress = true) assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } "fail to validate an invalid noAddress entry" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest"), "noAddress" -> WdlInteger(1)) - assertJesRuntimeAttributesFailedCreation(runtimeAttributes, "Failed to validate noAddress runtime attribute: Could not coerce 1 into a boolean") + assertJesRuntimeAttributesFailedCreation(runtimeAttributes, + "Expecting noAddress runtime attribute to be a Boolean") } "use reasonable default values" in { val runtimeAttributes = Map("docker" -> WdlString("ubuntu:latest")) - val expectedRuntimeAttributes = expectedDefaultsPlusUbuntuDocker + val expectedRuntimeAttributes = expectedDefaults assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes) } } diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigAsyncJobExecutionActor.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigAsyncJobExecutionActor.scala index 12ca089eb..da3ee2e87 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigAsyncJobExecutionActor.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigAsyncJobExecutionActor.scala @@ -83,7 +83,7 @@ $command private lazy val dockerInputs: WorkflowCoercedInputs = { if (isDockerRun) { Map( - DockerCwdInput -> WdlString(jobPaths.callDockerRoot.toString) + DockerCwdInput -> WdlString(jobPathsWithDocker.callDockerRoot.toString) ) } else { Map.empty diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala index 0debe3aee..5296020c6 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala @@ -4,10 +4,11 @@ import com.typesafe.config.Config import cromwell.backend.callcaching.FileHashingActor.FileHashingFunction import cromwell.backend.impl.sfs.config.ConfigConstants._ import cromwell.backend.sfs._ +import cromwell.backend.standard.StandardInitializationData import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, RuntimeAttributeDefinition} import cromwell.core.JobExecutionToken.JobExecutionTokenType import net.ceedubs.ficus.Ficus._ -import org.slf4j.LoggerFactory +import org.slf4j.{Logger, LoggerFactory} /** * Builds a backend by reading the job control from the config. @@ -17,14 +18,14 @@ import org.slf4j.LoggerFactory class ConfigBackendLifecycleActorFactory(name: String, val configurationDescriptor: BackendConfigurationDescriptor) extends SharedFileSystemBackendLifecycleActorFactory { - lazy val logger = LoggerFactory.getLogger(getClass) - lazy val hashingStrategy = { + lazy val logger: Logger = LoggerFactory.getLogger(getClass) + lazy val hashingStrategy: ConfigHashingStrategy = { configurationDescriptor.backendConfig.as[Option[Config]]("filesystems.local.caching") map ConfigHashingStrategy.apply getOrElse ConfigHashingStrategy.defaultStrategy } - override def initializationActorClass = classOf[ConfigInitializationActor] + override def initializationActorClass: Class[ConfigInitializationActor] = classOf[ConfigInitializationActor] - override def asyncJobExecutionActorClass: Class[_ <: ConfigAsyncJobExecutionActor] = { + override def asyncExecutionActorClass: Class[_ <: ConfigAsyncJobExecutionActor] = { val runInBackground = configurationDescriptor.backendConfig.as[Option[Boolean]](RunInBackgroundConfig).getOrElse(false) if (runInBackground) classOf[BackgroundConfigAsyncJobExecutionActor] @@ -35,7 +36,7 @@ class ConfigBackendLifecycleActorFactory(name: String, val configurationDescript override def runtimeAttributeDefinitions(initializationDataOption: Option[BackendInitializationData]): Set[RuntimeAttributeDefinition] = { val initializationData = BackendInitializationData. - as[SharedFileSystemBackendInitializationData](initializationDataOption) + as[StandardInitializationData](initializationDataOption) initializationData.runtimeAttributesBuilder.definitions.toSet } diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategy.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategy.scala index f453cf315..3f528022f 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategy.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategy.scala @@ -4,18 +4,18 @@ import akka.event.LoggingAdapter import better.files.File import com.typesafe.config.Config import cromwell.backend.callcaching.FileHashingActor.SingleFileHashRequest -import cromwell.backend.sfs.SharedFileSystemBackendInitializationData +import cromwell.backend.standard.StandardInitializationData import cromwell.core.path.PathFactory -import cromwell.util.TryWithResource._ import cromwell.util.FileUtil._ +import cromwell.util.TryWithResource._ import net.ceedubs.ficus.Ficus._ import org.apache.commons.codec.digest.DigestUtils -import org.slf4j.LoggerFactory +import org.slf4j.{Logger, LoggerFactory} import scala.util.{Failure, Try} object ConfigHashingStrategy { - val logger = LoggerFactory.getLogger(getClass) + val logger: Logger = LoggerFactory.getLogger(getClass) val defaultStrategy = HashFileStrategy(false) def apply(hashingConfig: Config): ConfigHashingStrategy = { @@ -36,10 +36,11 @@ abstract class ConfigHashingStrategy { protected def hash(file: File): Try[String] protected def description: String - protected lazy val checkSiblingMessage = if (checkSiblingMd5) "Check first for sibling md5 and if not found " else "" + protected lazy val checkSiblingMessage: String = + if (checkSiblingMd5) "Check first for sibling md5 and if not found " else "" def getHash(request: SingleFileHashRequest, log: LoggingAdapter): Try[String] = { - def usingSFSInitData(initData: SharedFileSystemBackendInitializationData) = { + def usingStandardInitData(initData: StandardInitializationData) = { val pathBuilders = initData.workflowPaths.pathBuilders val file = PathFactory.buildFile(request.file.valueString, pathBuilders).followSymlinks @@ -52,7 +53,7 @@ abstract class ConfigHashingStrategy { } request.initializationData match { - case Some(initData: SharedFileSystemBackendInitializationData) => usingSFSInitData(initData) + case Some(initData: StandardInitializationData) => usingStandardInitData(initData) case _ => Failure(new IllegalArgumentException("Need SharedFileSystemBackendInitializationData to calculate hash.")) } } @@ -62,7 +63,7 @@ abstract class ConfigHashingStrategy { if (md5.exists) Option(md5) else None } - override def toString = { + override def toString: String = { s"Call caching hashing strategy: $checkSiblingMessage$description." } } diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigInitializationActor.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigInitializationActor.scala index f9f5a882f..c78013582 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigInitializationActor.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigInitializationActor.scala @@ -2,11 +2,12 @@ package cromwell.backend.impl.sfs.config import cromwell.backend.io.WorkflowPaths import cromwell.backend.sfs._ +import cromwell.backend.standard.{DefaultInitializationActorParams, StandardInitializationData, StandardValidatedRuntimeAttributesBuilder} import wdl4s.WdlNamespace /** * Extension of the SharedFileSystemBackendInitializationData with declarations of extra runtime attributes, and a - * wdl namespace containing various tasks for submiting, killing, etc. + * wdl namespace containing various tasks for submitting, killing, etc. * * @param workflowPaths The paths for the workflow. * @param runtimeAttributesBuilder The customized runtime attributes builder with extra validations for the @@ -17,10 +18,10 @@ import wdl4s.WdlNamespace class ConfigInitializationData ( workflowPaths: WorkflowPaths, - runtimeAttributesBuilder: SharedFileSystemValidatedRuntimeAttributesBuilder, + runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder, val declarationValidations: Seq[DeclarationValidation], val wdlNamespace: WdlNamespace) - extends SharedFileSystemBackendInitializationData(workflowPaths, runtimeAttributesBuilder) + extends StandardInitializationData(workflowPaths, runtimeAttributesBuilder) /** * Extends the SharedFileSystemInitializationActor to create an instance of the ConfigInitializationData. @@ -29,7 +30,7 @@ class ConfigInitializationData * * @param params Parameters to create an initialization actor. */ -class ConfigInitializationActor(params: SharedFileSystemInitializationActorParams) +class ConfigInitializationActor(params: DefaultInitializationActorParams) extends SharedFileSystemInitializationActor(params) { lazy val configWdlNamespace = new ConfigWdlNamespace(params.configurationDescriptor.backendConfig) @@ -38,12 +39,12 @@ class ConfigInitializationActor(params: SharedFileSystemInitializationActorParam DeclarationValidation.fromDeclarations(configWdlNamespace.runtimeDeclarations) } - override lazy val initializationData = { + override lazy val initializationData: ConfigInitializationData = { val wdlNamespace = configWdlNamespace.wdlNamespace new ConfigInitializationData(workflowPaths, runtimeAttributesBuilder, declarationValidations, wdlNamespace) } - override lazy val runtimeAttributesBuilder = { + override lazy val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder = { val declared = declarationValidations.map(_.makeValidation()) super.runtimeAttributesBuilder.withValidation(declared: _*) } diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/DeclarationValidation.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/DeclarationValidation.scala index 707f1a93f..904705180 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/DeclarationValidation.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/DeclarationValidation.scala @@ -26,8 +26,9 @@ object DeclarationValidation { def fromDeclaration(declaration: Declaration): DeclarationValidation = { declaration.unqualifiedName match { // Docker and CPU are special keys understood by cromwell. - case DockerValidation.key => new DeclarationValidation(declaration, DockerValidation.instance) - case CpuValidation.key => new DeclarationValidation(declaration, CpuValidation.default) + case name if name == DockerValidation.instance.key => + new DeclarationValidation(declaration, DockerValidation.instance) + case name if name == CpuValidation.instance.key => new DeclarationValidation(declaration, CpuValidation.default) // See MemoryDeclarationValidation for more info case name if MemoryDeclarationValidation.isMemoryDeclaration(name) => new MemoryDeclarationValidation(declaration) @@ -38,13 +39,15 @@ object DeclarationValidation { } } - private def validator(wdlType: WdlType, unqualifiedName: String): PrimitiveRuntimeAttributesValidation[_] = wdlType match { - case WdlBooleanType => new BooleanRuntimeAttributesValidation(unqualifiedName) - case WdlFloatType => new FloatRuntimeAttributesValidation(unqualifiedName) - case WdlIntegerType => new IntRuntimeAttributesValidation(unqualifiedName) - case WdlStringType => new StringRuntimeAttributesValidation(unqualifiedName) - case WdlOptionalType(x) => validator(x, unqualifiedName) - case other => throw new RuntimeException(s"Unsupported config runtime attribute $other $unqualifiedName") + private def validator(wdlType: WdlType, unqualifiedName: String): PrimitiveRuntimeAttributesValidation[_, _] = { + wdlType match { + case WdlBooleanType => new BooleanRuntimeAttributesValidation(unqualifiedName) + case WdlFloatType => new FloatRuntimeAttributesValidation(unqualifiedName) + case WdlIntegerType => new IntRuntimeAttributesValidation(unqualifiedName) + case WdlStringType => new StringRuntimeAttributesValidation(unqualifiedName) + case WdlOptionalType(x) => validator(x, unqualifiedName) + case other => throw new RuntimeException(s"Unsupported config runtime attribute $other $unqualifiedName") + } } } @@ -55,7 +58,7 @@ object DeclarationValidation { * @param instanceValidation A basic instance validation for the declaration. */ class DeclarationValidation(declaration: Declaration, instanceValidation: RuntimeAttributesValidation[_]) { - val key = declaration.unqualifiedName + val key: String = declaration.unqualifiedName /** * Creates a validation, by adding on defaults if they're specified in the declaration, and then making the @@ -142,7 +145,8 @@ class MemoryDeclarationValidation(declaration: Declaration) * @param wdlExpression The declaration expression to retrieve the default. * @return The new validation. */ - override protected def default(validation: RuntimeAttributesValidation[_], wdlExpression: WdlExpression) = { + override protected def default(validation: RuntimeAttributesValidation[_], + wdlExpression: WdlExpression): RuntimeAttributesValidation[_] = { val wdlValue = declaration.expression.get.evaluate(NoLookup, NoFunctions).get val amount: Double = wdlValue match { case WdlInteger(value) => value.toDouble diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemAsyncJobExecutionActor.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemAsyncJobExecutionActor.scala index f0a0814c3..2c850aaf8 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemAsyncJobExecutionActor.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemAsyncJobExecutionActor.scala @@ -2,30 +2,28 @@ package cromwell.backend.sfs import java.nio.file.{FileAlreadyExistsException, Path} -import akka.actor.{Actor, ActorLogging} import better.files._ import cromwell.backend._ -import cromwell.backend.async.{AsyncBackendJobExecutionActor, ExecutionHandle, FailedNonRetryableExecutionHandle, PendingExecutionHandle, SuccessfulExecutionHandle} -import cromwell.backend.io.WorkflowPathsBackendInitializationData -import cromwell.backend.standard.{StandardAsyncExecutionActor, StandardAsyncJob} +import cromwell.backend.async.{ExecutionHandle, FailedNonRetryableExecutionHandle, PendingExecutionHandle, SuccessfulExecutionHandle} +import cromwell.backend.io.JobPathsWithDocker +import cromwell.backend.standard.{StandardAsyncExecutionActor, StandardAsyncJob, StandardInitializationData} import cromwell.backend.validation._ import cromwell.backend.wdl.OutputEvaluator -import cromwell.core.WorkflowId import cromwell.core.path.PathFactory._ import cromwell.core.path.{DefaultPathBuilder, PathBuilder} import cromwell.core.retry.SimpleExponentialBackoff +import wdl4s.EvaluatedTaskInputs import wdl4s.values.{WdlArray, WdlFile, WdlGlobFile, WdlMap, WdlValue} -import wdl4s.{EvaluatedTaskInputs, TaskCall} import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} -object SharedFileSystemJob { +case class SharedFileSystemRunStatus(returnCodeFileExists: Boolean) + +object SharedFileSystemAsyncJobExecutionActor { val JobIdKey = "sfs_job_id" } -case class SharedFileSystemRunStatus(returnCodeFileExists: Boolean) - /** * Runs a job on a shared backend, with the ability to (abstractly) submit asynchronously, then poll, kill, etc. * @@ -51,8 +49,7 @@ case class SharedFileSystemRunStatus(returnCodeFileExists: Boolean) * messages. */ trait SharedFileSystemAsyncJobExecutionActor - extends Actor with ActorLogging with BackendJobLifecycleActor with AsyncBackendJobExecutionActor - with StandardAsyncExecutionActor with SharedFileSystemJobCachingActorHelper { + extends BackendJobLifecycleActor with StandardAsyncExecutionActor with SharedFileSystemJobCachingActorHelper { override type StandardAsyncRunInfo = Any @@ -98,11 +95,13 @@ trait SharedFileSystemAsyncJobExecutionActor */ def killArgs(job: StandardAsyncJob): SharedFileSystemCommand + lazy val jobPathsWithDocker: JobPathsWithDocker = jobPaths.asInstanceOf[JobPathsWithDocker] + def toUnixPath(docker: Boolean)(path: WdlValue): WdlValue = { path match { case _: WdlFile => val cleanPath = DefaultPathBuilder.build(path.valueString).get - WdlFile(if (docker) jobPaths.toDockerPath(cleanPath).toString else cleanPath.toString) + WdlFile(if (docker) jobPathsWithDocker.toDockerPath(cleanPath).toString else cleanPath.toString) case array: WdlArray => WdlArray(array.wdlType, array.value map toUnixPath(docker)) case map: WdlMap => WdlMap(map.wdlType, map.value mapValues toUnixPath(docker)) case wdlValue => wdlValue @@ -111,12 +110,8 @@ trait SharedFileSystemAsyncJobExecutionActor def jobName: String = s"cromwell_${jobDescriptor.workflowDescriptor.id.shortString}_${jobDescriptor.call.unqualifiedName}" - lazy val workflowDescriptor: BackendWorkflowDescriptor = jobDescriptor.workflowDescriptor - lazy val call: TaskCall = jobDescriptor.key.call - lazy val pathBuilders: List[PathBuilder] = WorkflowPathsBackendInitializationData.pathBuilders(backendInitializationDataOption) - private[sfs] lazy val backendEngineFunctions = SharedFileSystemExpressionFunctions(jobPaths, pathBuilders) - override lazy val workflowId: WorkflowId = jobDescriptor.workflowDescriptor.id - override lazy val jobTag: String = jobDescriptor.key.tag + lazy val pathBuilders: List[PathBuilder] = StandardInitializationData.pathBuilders(backendInitializationDataOption) + lazy val backendEngineFunctions = SharedFileSystemExpressionFunctions(jobPaths, pathBuilders) lazy val isDockerRun: Boolean = RuntimeAttributesValidation.extractOption( DockerValidation.instance, validatedRuntimeAttributes).isDefined @@ -124,19 +119,15 @@ trait SharedFileSystemAsyncJobExecutionActor override lazy val commandLineFunctions: SharedFileSystemExpressionFunctions = backendEngineFunctions override lazy val commandLinePreProcessor: (EvaluatedTaskInputs) => Try[EvaluatedTaskInputs] = - sharedFileSystem.localizeInputs(jobPaths.callInputsRoot, isDockerRun) + sharedFileSystem.localizeInputs(jobPathsWithDocker.callInputsRoot, isDockerRun) override lazy val commandLineValueMapper: (WdlValue) => WdlValue = toUnixPath(isDockerRun) - override lazy val startMetadataKeyValues: Map[String, Any] = { - super[SharedFileSystemJobCachingActorHelper].startMetadataKeyValues - } - override def execute(): ExecutionHandle = { val script = instantiatedCommand jobLogger.info(s"`$script`") File(jobPaths.callExecutionRoot).createDirectories() - val cwd = if (isDockerRun) jobPaths.callExecutionDockerRoot else jobPaths.callExecutionRoot + val cwd = if (isDockerRun) jobPathsWithDocker.callExecutionDockerRoot else jobPaths.callExecutionRoot writeScript(script, cwd, backendEngineFunctions.findGlobOutputs(call, jobDescriptor)) jobLogger.info(s"command: $processArgs") val runner = makeProcessRunner() @@ -168,7 +159,7 @@ trait SharedFileSystemAsyncJobExecutionActor * as some extra shell code for monitoring jobs */ private def writeScript(instantiatedCommand: String, cwd: Path, globFiles: Set[WdlGlobFile]) = { - val rcPath = if (isDockerRun) jobPaths.toDockerPath(jobPaths.returnCode) else jobPaths.returnCode + val rcPath = if (isDockerRun) jobPathsWithDocker.toDockerPath(jobPaths.returnCode) else jobPaths.returnCode val rcTmpPath = pathPlusSuffix(rcPath, "tmp").path def globManipulation(globFile: WdlGlobFile) = { @@ -243,16 +234,6 @@ trait SharedFileSystemAsyncJobExecutionActor () } - override def remoteStdErrPath: Path = jobPaths.stderr - - override def remoteReturnCodePath: Path = jobPaths.returnCode - - override def continueOnReturnCode: ContinueOnReturnCode = RuntimeAttributesValidation.extract( - ContinueOnReturnCodeValidation.instance, validatedRuntimeAttributes) - - override def failOnStdErr: Boolean = RuntimeAttributesValidation.extract( - FailOnStderrValidation.instance, validatedRuntimeAttributes) - override def pollStatus(handle: StandardAsyncPendingExecutionHandle): SharedFileSystemRunStatus = { SharedFileSystemRunStatus(File(jobPaths.returnCode).exists) } diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemBackendLifecycleActorFactory.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemBackendLifecycleActorFactory.scala index 43d77dd5d..150f8b199 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemBackendLifecycleActorFactory.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemBackendLifecycleActorFactory.scala @@ -1,16 +1,7 @@ package cromwell.backend.sfs -import akka.actor.{ActorRef, Props} -import cats.data.Validated.{Invalid, Valid} import cromwell.backend._ -import cromwell.backend.standard.StandardLifecycleActorFactory -import cromwell.core.Dispatcher -import cromwell.core.Dispatcher._ -import cromwell.core.path.{DefaultPathBuilderFactory, PathBuilderFactory} -import cromwell.filesystems.gcs.{GcsPathBuilderFactory, GoogleConfiguration} -import lenthall.exception.MessageAggregation -import net.ceedubs.ficus.Ficus._ -import wdl4s.TaskCall +import cromwell.backend.standard._ import wdl4s.expression.WdlStandardLibraryFunctions /** @@ -20,50 +11,10 @@ import wdl4s.expression.WdlStandardLibraryFunctions */ trait SharedFileSystemBackendLifecycleActorFactory extends StandardLifecycleActorFactory { - override def jobIdKey: String = SharedFileSystemJob.JobIdKey + override def jobIdKey: String = SharedFileSystemAsyncJobExecutionActor.JobIdKey - /** - * If the backend sets a gcs authentication mode, try to create a PathBuilderFactory with it. - */ - lazy val gcsPathBuilderFactory: Option[GcsPathBuilderFactory] = { - configurationDescriptor.backendConfig.as[Option[String]]("filesystems.gcs.auth") map { configAuth => - GoogleConfiguration(configurationDescriptor.globalConfig).auth(configAuth) match { - case Valid(auth) => GcsPathBuilderFactory(auth) - case Invalid(error) => throw new MessageAggregation { - override def exceptionContext: String = "Failed to parse gcs auth configuration" - - override def errorMessages: Traversable[String] = error.toList - } - } - } - } - - lazy val pathBuilderFactories: List[PathBuilderFactory] = List(gcsPathBuilderFactory, Option(DefaultPathBuilderFactory)).flatten - - /** - * Returns the initialization class, or by default uses the `SharedFileSystemInitializationActor`. - * - * @return the initialization class. - */ - def initializationActorClass: Class[_ <: SharedFileSystemInitializationActor] = - classOf[SharedFileSystemInitializationActor] - - override def workflowInitializationActorProps(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], - serviceRegistryActor: ActorRef): Option[Props] = { - val params = SharedFileSystemInitializationActorParams(serviceRegistryActor, workflowDescriptor, - configurationDescriptor, calls, pathBuilderFactories) - Option(Props(initializationActorClass, params).withDispatcher(Dispatcher.BackendDispatcher)) - } - - override def cacheHitCopyingActorProps = Option(cacheHitCopyingActorInner _) - - def cacheHitCopyingActorInner(jobDescriptor: BackendJobDescriptor, - initializationDataOption: Option[BackendInitializationData], - serviceRegistryActor: ActorRef): Props = { - Props( - new SharedFileSystemCacheHitCopyingActor( - jobDescriptor, configurationDescriptor, initializationDataOption, serviceRegistryActor) - ).withDispatcher(BackendDispatcher) + override lazy val standardCacheHitCopyingActorOption: Option[Class[_ <: StandardCacheHitCopyingActor]] = { + Option(classOf[SharedFileSystemCacheHitCopyingActor]) } override def expressionLanguageFunctions(workflowDescriptor: BackendWorkflowDescriptor, diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemCacheHitCopyingActor.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemCacheHitCopyingActor.scala index 6ba44914d..f6a8ad65c 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemCacheHitCopyingActor.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemCacheHitCopyingActor.scala @@ -2,27 +2,11 @@ package cromwell.backend.sfs import java.nio.file.Path -import akka.actor.ActorRef -import cromwell.backend.callcaching.CacheHitDuplicating -import cromwell.backend.{BackendCacheHitCopyingActor, BackendConfigurationDescriptor, BackendInitializationData, BackendJobDescriptor} -import cromwell.core.path.PathFactory +import cromwell.backend.standard.{StandardCacheHitCopyingActor, StandardCacheHitCopyingActorParams} -import scala.util.Try - -class SharedFileSystemCacheHitCopyingActor(override val jobDescriptor: BackendJobDescriptor, - override val configurationDescriptor: BackendConfigurationDescriptor, - override val backendInitializationDataOption: - Option[BackendInitializationData], - override val serviceRegistryActor: ActorRef) - extends SharedFileSystemJobCachingActorHelper with BackendCacheHitCopyingActor with CacheHitDuplicating { - - override lazy val destinationCallRootPath = jobPaths.callRoot - - override lazy val destinationJobDetritusPaths = jobPaths.detritusPaths - - override protected def getPath(file: String) = Try(PathFactory.buildPath(file, jobPaths.pathBuilders)) - - override protected def duplicate(source: Path, destination: Path) = { +class SharedFileSystemCacheHitCopyingActor(override val standardParams: StandardCacheHitCopyingActorParams) + extends StandardCacheHitCopyingActor with SharedFileSystemJobCachingActorHelper { + override protected def duplicate(source: Path, destination: Path): Unit = { // -Ywarn-value-discard sharedFileSystem.cacheCopy(source, destination) () diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemExpressionFunctions.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemExpressionFunctions.scala index 859b98830..0250c7a10 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemExpressionFunctions.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemExpressionFunctions.scala @@ -3,6 +3,7 @@ package cromwell.backend.sfs import java.nio.file.Path import cromwell.backend.io._ +import cromwell.backend.standard.StandardInitializationData import cromwell.backend.wdl._ import cromwell.backend._ import cromwell.core.CallContext @@ -15,7 +16,8 @@ import scala.util.{Success, Try} object SharedFileSystemExpressionFunctions { private val LocalFileSystemScheme = "file" - def isLocalPath(path: Path) = path.toUri.getScheme == SharedFileSystemExpressionFunctions.LocalFileSystemScheme + def isLocalPath(path: Path): Boolean = + path.toUri.getScheme == SharedFileSystemExpressionFunctions.LocalFileSystemScheme def apply(workflowDescriptor: BackendWorkflowDescriptor, jobKey: BackendJobDescriptorKey, @@ -42,7 +44,7 @@ object SharedFileSystemExpressionFunctions { def apply(workflowDescriptor: BackendWorkflowDescriptor, configurationDescriptor: BackendConfigurationDescriptor, jobKey: BackendJobDescriptorKey, - initializationData: Option[BackendInitializationData]) = { + initializationData: Option[BackendInitializationData]): SharedFileSystemExpressionFunctions = { val jobPaths = new JobPathsWithDocker(jobKey, workflowDescriptor, configurationDescriptor.backendConfig) val callContext = CallContext( jobPaths.callExecutionRoot, @@ -50,7 +52,7 @@ object SharedFileSystemExpressionFunctions { jobPaths.stderr.toString ) - new SharedFileSystemExpressionFunctions(WorkflowPathsBackendInitializationData.pathBuilders(initializationData), callContext) + new SharedFileSystemExpressionFunctions(StandardInitializationData.pathBuilders(initializationData), callContext) } } @@ -63,10 +65,11 @@ class SharedFileSystemExpressionFunctions(override val pathBuilders: List[PathBu override def writeTempFile(path: String, prefix: String, suffix: String, content: String): String = super[WriteFunctions].writeTempFile(path, prefix, suffix, content) - override val writeDirectory = context.root + override val writeDirectory: Path = context.root override def stdout(params: Seq[Try[WdlValue]]) = Success(WdlFile(context.stdout)) override def stderr(params: Seq[Try[WdlValue]]) = Success(WdlFile(context.stderr)) - override def postMapping(path: Path) = if (!path.isAbsolute && isLocalPath(path)) context.root.resolve(path) else path + override def postMapping(path: Path): Path = + if (!path.isAbsolute && isLocalPath(path)) context.root.resolve(path) else path } diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemInitializationActor.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemInitializationActor.scala index 52c8fbeaf..6509ee3f6 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemInitializationActor.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemInitializationActor.scala @@ -1,59 +1,57 @@ package cromwell.backend.sfs -import akka.actor.ActorRef import better.files._ -import cromwell.backend.io.{WorkflowPaths, WorkflowPathsBackendInitializationData} -import cromwell.backend.validation.RuntimeAttributesDefault +import cats.data.Validated.{Invalid, Valid} +import cromwell.backend.BackendInitializationData +import cromwell.backend.io.WorkflowPaths +import cromwell.backend.standard.{DefaultInitializationActorParams, StandardInitializationActor, StandardInitializationActorParams, StandardInitializationData} import cromwell.backend.wfs.WorkflowPathBuilder -import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, BackendWorkflowDescriptor, BackendWorkflowInitializationActor} -import cromwell.core.WorkflowOptions -import cromwell.core.path.PathBuilderFactory -import wdl4s.TaskCall -import wdl4s.values.WdlValue +import cromwell.core.path.{DefaultPathBuilderFactory, PathBuilder, PathBuilderFactory} +import cromwell.filesystems.gcs.{GcsPathBuilderFactory, GoogleConfiguration} +import lenthall.exception.MessageAggregation +import net.ceedubs.ficus.Ficus._ import scala.concurrent.Future import scala.util.Try -case class SharedFileSystemInitializationActorParams -( - serviceRegistryActor: ActorRef, - workflowDescriptor: BackendWorkflowDescriptor, - configurationDescriptor: BackendConfigurationDescriptor, - calls: Set[TaskCall], - pathBuilderFactories: List[PathBuilderFactory] -) - -class SharedFileSystemBackendInitializationData -( - val workflowPaths: WorkflowPaths, - val runtimeAttributesBuilder: SharedFileSystemValidatedRuntimeAttributesBuilder) - extends WorkflowPathsBackendInitializationData - /** * Initializes a shared file system actor factory and creates initialization data to pass to the execution actors. * * @param params Initialization parameters. */ -class SharedFileSystemInitializationActor(params: SharedFileSystemInitializationActorParams) - extends BackendWorkflowInitializationActor { - - override lazy val workflowDescriptor: BackendWorkflowDescriptor = params.workflowDescriptor - override lazy val configurationDescriptor: BackendConfigurationDescriptor = params.configurationDescriptor - override lazy val calls: Set[TaskCall] = params.calls - override lazy val serviceRegistryActor: ActorRef = params.serviceRegistryActor +class SharedFileSystemInitializationActor(params: DefaultInitializationActorParams) + extends StandardInitializationActor { - def runtimeAttributesBuilder: SharedFileSystemValidatedRuntimeAttributesBuilder = - SharedFileSystemValidatedRuntimeAttributesBuilder.default + override val standardParams: StandardInitializationActorParams = params - override protected def runtimeAttributeValidators: Map[String, (Option[WdlValue]) => Boolean] = { - runtimeAttributesBuilder.validations.map(validation => - validation.key -> validation.validateOptionalExpression _ - ).toMap + /** + * If the backend sets a gcs authentication mode, try to create a PathBuilderFactory with it. + */ + lazy val gcsPathBuilderFactory: Option[GcsPathBuilderFactory] = { + configurationDescriptor.backendConfig.as[Option[String]]("filesystems.gcs.auth") map { configAuth => + GoogleConfiguration(configurationDescriptor.globalConfig).auth(configAuth) match { + case Valid(auth) => GcsPathBuilderFactory(auth) + case Invalid(error) => throw new MessageAggregation { + override def exceptionContext: String = "Failed to parse gcs auth configuration" + + override def errorMessages: Traversable[String] = error.toList + } + } + } } - val pathBuilders = params.pathBuilderFactories map { _.withOptions(workflowDescriptor.workflowOptions)(context.system) } + lazy val pathBuilderFactories: List[PathBuilderFactory] = + List(gcsPathBuilderFactory, Option(DefaultPathBuilderFactory)).flatten + + lazy val pathBuilders: List[PathBuilder] = + pathBuilderFactories map { _.withOptions(workflowDescriptor.workflowOptions)(context.system) } + + val workflowPaths: WorkflowPaths = + WorkflowPathBuilder.workflowPaths(configurationDescriptor, workflowDescriptor, pathBuilders) - val workflowPaths = WorkflowPathBuilder.workflowPaths(configurationDescriptor, workflowDescriptor, pathBuilders) + def initializationData: StandardInitializationData = { + new StandardInitializationData(workflowPaths, runtimeAttributesBuilder) + } override def beforeAll(): Future[Option[BackendInitializationData]] = { Future.fromTry(Try { @@ -62,31 +60,4 @@ class SharedFileSystemInitializationActor(params: SharedFileSystemInitialization Option(initializationData) }) } - - def initializationData: SharedFileSystemBackendInitializationData = { - new SharedFileSystemBackendInitializationData(workflowPaths, runtimeAttributesBuilder) - } - - /** - * Log a warning if there are non-supported runtime attributes defined for the call. - */ - override def validate(): Future[Unit] = { - Future.fromTry(Try { - calls foreach { call => - val runtimeAttributeKeys = call.task.runtimeAttributes.attrs.keys.toList - val notSupportedAttributes = runtimeAttributesBuilder.unsupportedKeys(runtimeAttributeKeys).toList - - if (notSupportedAttributes.nonEmpty) { - val notSupportedAttrString = notSupportedAttributes mkString ", " - workflowLogger.warn( - s"Key/s [$notSupportedAttrString] is/are not supported by backend. " + - s"Unsupported attributes will not be part of jobs executions.") - } - } - }) - } - - override protected def coerceDefaultRuntimeAttributes(options: WorkflowOptions): Try[Map[String, WdlValue]] = { - RuntimeAttributesDefault.workflowOptionsDefault(options, runtimeAttributesBuilder.validations.map(v => v.key -> v.coercion).toMap) - } } diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemJobCachingActorHelper.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemJobCachingActorHelper.scala index 2c2954cf6..ff78494d3 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemJobCachingActorHelper.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/sfs/SharedFileSystemJobCachingActorHelper.scala @@ -1,45 +1,18 @@ package cromwell.backend.sfs -import akka.actor.{Actor, ActorRef} +import akka.actor.Actor import com.typesafe.config.{Config, ConfigFactory} -import cromwell.backend.BackendInitializationData -import cromwell.backend.callcaching.JobCachingActorHelper -import cromwell.backend.io.{JobPathsWithDocker, WorkflowPathsBackendInitializationData} -import cromwell.backend.validation.{RuntimeAttributesValidation, ValidatedRuntimeAttributes} +import cromwell.backend.standard.{StandardCachingActorHelper, StandardInitializationData} import cromwell.core.logging.JobLogging import cromwell.core.path.PathBuilder import net.ceedubs.ficus.Ficus._ -trait SharedFileSystemJobCachingActorHelper extends JobCachingActorHelper { +trait SharedFileSystemJobCachingActorHelper extends StandardCachingActorHelper { this: Actor with JobLogging => - def backendInitializationDataOption: Option[BackendInitializationData] - - def serviceRegistryActor: ActorRef - - lazy val jobPaths = - new JobPathsWithDocker(jobDescriptor.key, jobDescriptor.workflowDescriptor, configurationDescriptor.backendConfig) - - lazy val initializationData: SharedFileSystemBackendInitializationData = BackendInitializationData. - as[SharedFileSystemBackendInitializationData](backendInitializationDataOption) - - lazy val validatedRuntimeAttributes: ValidatedRuntimeAttributes = { - val builder = initializationData.runtimeAttributesBuilder - builder.build(jobDescriptor.runtimeAttributes, jobLogger) - } - - def startMetadataKeyValues: Map[String, Any] = { - val runtimeAttributesMetadata = RuntimeAttributesValidation.extract(validatedRuntimeAttributes) map { - case (key, value) => (s"runtimeAttributes:$key", value) - } - val fileMetadata = jobPaths.metadataPaths - val otherMetadata = Map("cache:allowResultReuse" -> true) - runtimeAttributesMetadata ++ fileMetadata ++ otherMetadata - } - lazy val sharedFileSystem = new SharedFileSystem { override val pathBuilders: List[PathBuilder] = { - WorkflowPathsBackendInitializationData.pathBuilders(backendInitializationDataOption) + StandardInitializationData.pathBuilders(backendInitializationDataOption) } override lazy val sharedFileSystemConfig: Config = { configurationDescriptor.backendConfig.as[Option[Config]]("filesystems.local").getOrElse(ConfigFactory.empty()) diff --git a/supportedBackends/sfs/src/test/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategySpec.scala b/supportedBackends/sfs/src/test/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategySpec.scala index bd25dfc51..d9a3d765d 100644 --- a/supportedBackends/sfs/src/test/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategySpec.scala +++ b/supportedBackends/sfs/src/test/scala/cromwell/backend/impl/sfs/config/ConfigHashingStrategySpec.scala @@ -7,7 +7,7 @@ import better.files._ import com.typesafe.config.{ConfigFactory, ConfigValueFactory} import cromwell.backend.callcaching.FileHashingActor.SingleFileHashRequest import cromwell.backend.io.WorkflowPaths -import cromwell.backend.sfs.SharedFileSystemBackendInitializationData +import cromwell.backend.standard.StandardInitializationData import cromwell.core.path.DefaultPathBuilder import org.apache.commons.codec.digest.DigestUtils import org.scalatest.prop.TableDrivenPropertyChecks @@ -49,7 +49,7 @@ class ConfigHashingStrategySpec extends FlatSpec with Matchers with TableDrivenP val workflowPaths = mock[WorkflowPaths] workflowPaths.pathBuilders returns List(DefaultPathBuilder) - val initData = mock[SharedFileSystemBackendInitializationData] + val initData = mock[StandardInitializationData] initData.workflowPaths returns workflowPaths request.file returns WdlFile(requestFile.pathAsString) diff --git a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemInitializationActorSpec.scala b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemInitializationActorSpec.scala index 1cbfd2cfe..085ad173d 100644 --- a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemInitializationActorSpec.scala +++ b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemInitializationActorSpec.scala @@ -4,6 +4,7 @@ import akka.actor.Props import akka.testkit.{EventFilter, ImplicitSender, TestDuration} import cromwell.backend.BackendSpec._ import cromwell.backend.BackendWorkflowInitializationActor.Initialize +import cromwell.backend.standard.DefaultInitializationActorParams import cromwell.backend.{BackendConfigurationDescriptor, BackendWorkflowDescriptor} import cromwell.core.TestKitSuite import cromwell.core.logging.LoggingTest._ @@ -14,9 +15,9 @@ import scala.concurrent.duration._ class SharedFileSystemInitializationActorSpec extends TestKitSuite("SharedFileSystemInitializationActorSpec") with WordSpecLike with Matchers with ImplicitSender { - val Timeout = 5.second.dilated + val Timeout: FiniteDuration = 5.second.dilated - val HelloWorld = + val HelloWorld: String = s""" |task hello { | String addressee = "you" @@ -37,7 +38,7 @@ class SharedFileSystemInitializationActorSpec extends TestKitSuite("SharedFileSy private def getActorRef(workflowDescriptor: BackendWorkflowDescriptor, calls: Set[TaskCall], conf: BackendConfigurationDescriptor) = { - val params = SharedFileSystemInitializationActorParams(emptyActor, workflowDescriptor, conf, calls, List.empty) + val params = DefaultInitializationActorParams(workflowDescriptor, calls, emptyActor, conf) val props = Props(new SharedFileSystemInitializationActor(params)) system.actorOf(props, "SharedFileSystemInitializationActor") } @@ -49,7 +50,7 @@ class SharedFileSystemInitializationActorSpec extends TestKitSuite("SharedFileSy val conf = emptyBackendConfig val backend = getActorRef(workflowDescriptor, workflowDescriptor.workflow.taskCalls, conf) val pattern = "Key/s [unsupported] is/are not supported by backend. " + - "Unsupported attributes will not be part of jobs executions." + "Unsupported attributes will not be part of job executions." EventFilter.warning(pattern = escapePattern(pattern), occurrences = 1) intercept { backend ! Initialize } diff --git a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala index c5053e416..f5ada20d9 100644 --- a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala +++ b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala @@ -10,6 +10,7 @@ import cromwell.backend.BackendLifecycleActor.AbortJobCommand import cromwell.backend.io.TestWorkflows._ import cromwell.backend.io.{JobPathsWithDocker, TestWorkflows} import cromwell.backend.sfs.TestLocalAsyncJobExecutionActor._ +import cromwell.backend.standard.StandardValidatedRuntimeAttributesBuilder import cromwell.backend.{BackendConfigurationDescriptor, BackendJobDescriptor, BackendJobDescriptorKey, BackendSpec, RuntimeAttributeDefinition} import cromwell.core.Tags._ import cromwell.core._ @@ -29,7 +30,7 @@ class SharedFileSystemJobExecutionActorSpec extends TestKitSuite("SharedFileSyst behavior of "SharedFileSystemJobExecutionActor" lazy val runtimeAttributeDefinitions: Set[RuntimeAttributeDefinition] = - SharedFileSystemValidatedRuntimeAttributesBuilder.default.definitions.toSet + StandardValidatedRuntimeAttributesBuilder.default.definitions.toSet def executeSpec(docker: Boolean): Any = { val expectedOutputs: CallOutputs = Map( @@ -183,7 +184,7 @@ class SharedFileSystemJobExecutionActorSpec extends TestKitSuite("SharedFileSyst val kvJobKey = KvJobKey(jobDescriptor.key.call.fullyQualifiedName, jobDescriptor.key.index, jobDescriptor.key.attempt) - val scopedKey = ScopedKey(workflowDescriptor.id, kvJobKey, SharedFileSystemJob.JobIdKey) + val scopedKey = ScopedKey(workflowDescriptor.id, kvJobKey, SharedFileSystemAsyncJobExecutionActor.JobIdKey) val kvPair = KvPair(scopedKey, Option(pid)) backendRef ! kvPair diff --git a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/TestLocalAsyncJobExecutionActor.scala b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/TestLocalAsyncJobExecutionActor.scala index 39fc21d6b..2c2b9ab20 100644 --- a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/TestLocalAsyncJobExecutionActor.scala +++ b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/TestLocalAsyncJobExecutionActor.scala @@ -2,7 +2,7 @@ package cromwell.backend.sfs import akka.actor.{ActorSystem, Props} import akka.testkit.TestActorRef -import cromwell.backend.standard.{StandardAsyncExecutionActorParams, StandardSyncExecutionActor, DefaultStandardSyncExecutionActorParams} +import cromwell.backend.standard._ import cromwell.backend.io.WorkflowPathsWithDocker import cromwell.backend.validation.{DockerValidation, RuntimeAttributesValidation} import cromwell.backend.{BackendConfigurationDescriptor, BackendJobDescriptor} @@ -14,7 +14,7 @@ class TestLocalAsyncJobExecutionActor(override val standardParams: StandardAsync if (isDockerRun) { val docker = RuntimeAttributesValidation.extract(DockerValidation.instance, validatedRuntimeAttributes) val cwd = jobPaths.callRoot.toString - val dockerCwd = jobPaths.callDockerRoot.toString + val dockerCwd = jobPathsWithDocker.callDockerRoot.toString SharedFileSystemCommand("/bin/bash", "-c", s"docker run --rm -v $cwd:$dockerCwd -i $docker /bin/bash < $script") } else { @@ -33,12 +33,12 @@ object TestLocalAsyncJobExecutionActor { (implicit system: ActorSystem): TestActorRef[StandardSyncExecutionActor] = { val emptyActor = system.actorOf(Props.empty) val workflowPaths = new WorkflowPathsWithDocker(jobDescriptor.workflowDescriptor, configurationDescriptor.backendConfig) - val initializationData = new SharedFileSystemBackendInitializationData(workflowPaths, - SharedFileSystemValidatedRuntimeAttributesBuilder.default.withValidation(DockerValidation.optional)) + val initializationData = new StandardInitializationData(workflowPaths, + StandardValidatedRuntimeAttributesBuilder.default.withValidation(DockerValidation.optional)) val asyncClass = classOf[TestLocalAsyncJobExecutionActor] - val params = DefaultStandardSyncExecutionActorParams(SharedFileSystemJob.JobIdKey, emptyActor, jobDescriptor, - configurationDescriptor, Option(initializationData), asyncClass) + val params = DefaultStandardSyncExecutionActorParams(SharedFileSystemAsyncJobExecutionActor.JobIdKey, emptyActor, + jobDescriptor, configurationDescriptor, Option(initializationData), None, asyncClass) TestActorRef(new StandardSyncExecutionActor(params)) } From dc595a4ff7c04f546356f759bea876737df00b62 Mon Sep 17 00:00:00 2001 From: Khalid Shakir Date: Sun, 8 Jan 2017 03:14:46 -0500 Subject: [PATCH 05/84] Various patches. MetadataValue.apply was throwing an NPE exception when passed null, even though it had a call to .getOrElse(""). Consolidated standard backend runtimeAttributeDefinitions implementation. Added a GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials in tests that use application default credentials. Refactored away JesBackendLifecycleActorFactory's toJes, only used in one place where a similar standard method now exists. Refactored away JesBackendLifecycleActorFactory's staticRuntimeAttributeDefinitions, only used in specs. CromwellServer no longer hard codes the binding timeout. --- .../standard/StandardLifecycleActorFactory.scala | 7 +++ core/src/main/resources/reference.conf | 1 + .../scala/cromwell/server/CromwellServer.scala | 30 ++++++------ .../filesystems/gcs/GcsPathBuilderSpec.scala | 6 +-- .../filesystems/gcs/auth/GoogleAuthModeSpec.scala | 18 ++++++++ .../cromwell/services/metadata/MetadataQuery.scala | 14 +++--- .../metadata/impl/MetadataServiceActorSpec.scala | 54 +++++++++++++++------- .../impl/jes/JesBackendLifecycleActorFactory.scala | 23 ++------- .../backend/impl/jes/JesRuntimeAttributes.scala | 5 +- .../jes/JesAsyncBackendJobExecutionActorSpec.scala | 13 ++++-- .../backend/impl/jes/JesCallPathsSpec.scala | 7 +++ .../impl/jes/JesInitializationActorSpec.scala | 4 +- .../impl/jes/JesRuntimeAttributesSpec.scala | 8 +++- .../backend/impl/jes/JesWorkflowPathsSpec.scala | 3 ++ .../ConfigBackendLifecycleActorFactory.scala | 11 +---- 15 files changed, 126 insertions(+), 78 deletions(-) create mode 100644 filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/auth/GoogleAuthModeSpec.scala diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala b/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala index aa7e57356..01cbe6eef 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardLifecycleActorFactory.scala @@ -144,4 +144,11 @@ trait StandardLifecycleActorFactory extends BackendLifecycleActorFactory { initializationData.get.asInstanceOf[StandardInitializationData].workflowPaths.workflowRoot } + override def runtimeAttributeDefinitions(initializationDataOption: Option[BackendInitializationData]): + Set[RuntimeAttributeDefinition] = { + val initializationData = BackendInitializationData. + as[StandardInitializationData](initializationDataOption) + + initializationData.runtimeAttributesBuilder.definitions.toSet + } } diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 0e57508b7..60e3903de 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -8,6 +8,7 @@ webservice { port = 8000 interface = 0.0.0.0 + timeout = 5s instance.name = "reference" } diff --git a/engine/src/main/scala/cromwell/server/CromwellServer.scala b/engine/src/main/scala/cromwell/server/CromwellServer.scala index 882150776..ac47e1ef6 100644 --- a/engine/src/main/scala/cromwell/server/CromwellServer.scala +++ b/engine/src/main/scala/cromwell/server/CromwellServer.scala @@ -2,8 +2,7 @@ package cromwell.server import java.util.concurrent.TimeoutException -import akka.actor.Props -import akka.util.Timeout +import akka.actor.{ActorContext, ActorSystem, Props} import com.typesafe.config.Config import cromwell.core.Dispatcher.EngineDispatcher import cromwell.webservice.WorkflowJsonSupport._ @@ -11,28 +10,29 @@ import cromwell.webservice.{APIResponse, CromwellApiService, SwaggerService} import lenthall.spray.SprayCanHttpService._ import lenthall.spray.WrappedRoute._ import net.ceedubs.ficus.Ficus._ -import spray.http.{ContentType, MediaTypes, _} +import spray.http._ import spray.json._ +import spray.routing.Route import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} +import scala.concurrent.{Await, ExecutionContextExecutor, Future} import scala.util.{Failure, Success} // Note that as per the language specification, this is instantiated lazily and only used when necessary (i.e. server mode) object CromwellServer { - implicit val timeout = Timeout(5.seconds) - import scala.concurrent.ExecutionContext.Implicits.global - def run(cromwellSystem: CromwellSystem): Future[Any] = { - implicit val actorSystem = cromwellSystem.actorSystem + implicit val executionContext = scala.concurrent.ExecutionContext.Implicits.global + + val actorSystem: ActorSystem = cromwellSystem.actorSystem val service = actorSystem.actorOf(CromwellServerActor.props(cromwellSystem.conf), "cromwell-service") val webserviceConf = cromwellSystem.conf.getConfig("webservice") val interface = webserviceConf.getString("interface") val port = webserviceConf.getInt("port") - val futureBind = service.bind(interface = interface, port = port) + val timeout = webserviceConf.as[FiniteDuration]("timeout") + val futureBind = service.bind(interface, port)(implicitly, timeout, actorSystem, implicitly) futureBind andThen { case Success(_) => actorSystem.log.info("Cromwell service started...") @@ -52,16 +52,18 @@ object CromwellServer { } class CromwellServerActor(config: Config) extends CromwellRootActor with CromwellApiService with SwaggerService { - implicit def executionContext = actorRefFactory.dispatcher + implicit def executionContext: ExecutionContextExecutor = actorRefFactory.dispatcher override val serverMode = true override val abortJobsOnTerminate = false - override def actorRefFactory = context - override def receive = handleTimeouts orElse runRoute(possibleRoutes) + override def actorRefFactory: ActorContext = context + override def receive: PartialFunction[Any, Unit] = handleTimeouts orElse runRoute(possibleRoutes) - val possibleRoutes = workflowRoutes.wrapped("api", config.as[Option[Boolean]]("api.routeUnwrapped").getOrElse(false)) ~ swaggerUiResourceRoute - val timeoutError = APIResponse.error(new TimeoutException("The server was not able to produce a timely response to your request.")).toJson.prettyPrint + val routeUnwrapped: Boolean = config.as[Option[Boolean]]("api.routeUnwrapped").getOrElse(false) + val possibleRoutes: Route = workflowRoutes.wrapped("api", routeUnwrapped) ~ swaggerUiResourceRoute + val timeoutError: String = APIResponse.error(new TimeoutException( + "The server was not able to produce a timely response to your request.")).toJson.prettyPrint def handleTimeouts: Receive = { case Timedout(_: HttpRequest) => diff --git a/filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/GcsPathBuilderSpec.scala b/filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/GcsPathBuilderSpec.scala index 598cb5461..3ce135e2f 100644 --- a/filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/GcsPathBuilderSpec.scala +++ b/filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/GcsPathBuilderSpec.scala @@ -5,16 +5,16 @@ import com.google.cloud.storage.contrib.nio.CloudStorageConfiguration import cromwell.core.path.CustomRetryParams import cromwell.core.path.proxy.RetryableFileSystemProviderProxy import cromwell.core.{TestKitSuite, WorkflowOptions} -import cromwell.filesystems.gcs.auth.GoogleAuthMode +import cromwell.filesystems.gcs.auth.{GoogleAuthMode, GoogleAuthModeSpec} import org.scalatest.{FlatSpecLike, Matchers} class GcsPathBuilderSpec extends TestKitSuite with FlatSpecLike with Matchers { - implicit val as = system - behavior of "GcsPathBuilderSpec" it should "create a path with a retryable provider" in { + GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials() + val retryablePathBuilder = new RetryableGcsPathBuilder( GoogleAuthMode.NoAuthMode, RetryParams.defaultInstance(), diff --git a/filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/auth/GoogleAuthModeSpec.scala b/filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/auth/GoogleAuthModeSpec.scala new file mode 100644 index 000000000..327f98fe7 --- /dev/null +++ b/filesystems/gcs/src/test/scala/cromwell/filesystems/gcs/auth/GoogleAuthModeSpec.scala @@ -0,0 +1,18 @@ +package cromwell.filesystems.gcs.auth + +import cromwell.core.WorkflowOptions +import org.scalatest.Assertions._ + +object GoogleAuthModeSpec { + def assumeHasApplicationDefaultCredentials(): Unit = { + try { + val authMode = ApplicationDefaultMode("application-default") + val workflowOptions = WorkflowOptions.empty + authMode.authCredentials(workflowOptions) + authMode.credential(workflowOptions) + () + } catch { + case exception: Exception => cancel(exception.getMessage) + } + } +} diff --git a/services/src/main/scala/cromwell/services/metadata/MetadataQuery.scala b/services/src/main/scala/cromwell/services/metadata/MetadataQuery.scala index f17118aa7..9015d59f0 100644 --- a/services/src/main/scala/cromwell/services/metadata/MetadataQuery.scala +++ b/services/src/main/scala/cromwell/services/metadata/MetadataQuery.scala @@ -6,7 +6,7 @@ import java.time.OffsetDateTime import cats.data.NonEmptyList import cromwell.core.WorkflowId import cromwell.core.path.PathImplicits._ -import org.slf4j.LoggerFactory +import org.slf4j.{Logger, LoggerFactory} import wdl4s.values.{WdlBoolean, WdlFloat, WdlInteger, WdlOptionalValue, WdlValue} case class MetadataJobKey(callFqn: String, index: Option[Int], attempt: Int) @@ -21,7 +21,7 @@ object MetadataKey { new MetadataKey(workflowId, jobKey, compositeKey(keys:_*)) } - def compositeKey(keys: String*) = keys.toList.mkString(KeySeparator.toString) + def compositeKey(keys: String*): String = keys.toList.mkString(KeySeparator.toString) } object MetadataEvent { @@ -48,15 +48,15 @@ object MetadataValue { case _: Double | Float => new MetadataValue(value.toString, MetadataNumber) case _: Boolean => new MetadataValue(value.toString, MetadataBoolean) case path: Path => new MetadataValue(path.toRealString, MetadataString) - case _ => new MetadataValue(value.toString, MetadataString) + case other => new MetadataValue(other.toString, MetadataString) } } } object MetadataType { - val log = LoggerFactory.getLogger("Metadata Type") + val log: Logger = LoggerFactory.getLogger("Metadata Type") - def fromString(s: String) = s match { + def fromString(s: String): MetadataType = s match { case MetadataString.typeName => MetadataString case MetadataInt.typeName => MetadataInt case MetadataNumber.typeName => MetadataNumber @@ -85,11 +85,11 @@ case class MetadataQuery(workflowId: WorkflowId, jobKey: Option[MetadataQueryJob object MetadataQuery { def forWorkflow(workflowId: WorkflowId) = MetadataQuery(workflowId, None, None, None, None, expandSubWorkflows = false) - def forJob(workflowId: WorkflowId, jobKey: MetadataJobKey) = { + def forJob(workflowId: WorkflowId, jobKey: MetadataJobKey): MetadataQuery = { MetadataQuery(workflowId, Option(MetadataQueryJobKey.forMetadataJobKey(jobKey)), None, None, None, expandSubWorkflows = false) } - def forKey(key: MetadataKey) = { + def forKey(key: MetadataKey): MetadataQuery = { MetadataQuery(key.workflowId, key.jobKey map MetadataQueryJobKey.forMetadataJobKey, Option(key.key), None, None, expandSubWorkflows = false) } } diff --git a/services/src/test/scala/cromwell/services/metadata/impl/MetadataServiceActorSpec.scala b/services/src/test/scala/cromwell/services/metadata/impl/MetadataServiceActorSpec.scala index 72e6b370a..12d201269 100644 --- a/services/src/test/scala/cromwell/services/metadata/impl/MetadataServiceActorSpec.scala +++ b/services/src/test/scala/cromwell/services/metadata/impl/MetadataServiceActorSpec.scala @@ -10,29 +10,29 @@ import cromwell.services.metadata.MetadataService._ import cromwell.services.metadata._ class MetadataServiceActorSpec extends ServicesSpec("Metadata") { + "MetadataServiceActor" should { - val config = ConfigFactory.empty() - val actor = system.actorOf(MetadataServiceActor.props(config, config)) + val config = ConfigFactory.empty() + val actor = system.actorOf(MetadataServiceActor.props(config, config)) - val workflowId = WorkflowId.randomId() + val workflowId = WorkflowId.randomId() - /* - Simple store / retrieve - */ + /* + Simple store / retrieve + */ - val key1 = MetadataKey(workflowId, None, "key1") - val key2 = MetadataKey(workflowId, None, "key2") - val supJob = MetadataJobKey("sup.sup", None, 1) - val key3 = MetadataKey(workflowId, Option(supJob), "dog") - val moment = OffsetDateTime.now + val key1 = MetadataKey(workflowId, None, "key1") + val key2 = MetadataKey(workflowId, None, "key2") + val supJob = MetadataJobKey("sup.sup", None, 1) + val key3 = MetadataKey(workflowId, Option(supJob), "dog") + val moment = OffsetDateTime.now - val event1_1 = MetadataEvent(key1, Option(MetadataValue("value1")), moment) - val event1_2 = MetadataEvent(key1, Option(MetadataValue("value2")), moment) - val event2_1 = MetadataEvent(key2, Option(MetadataValue("value1")), moment) - val event3_1 = MetadataEvent(key3, Option(MetadataValue("value3")), moment) - val event3_2 = MetadataEvent(key3, None, moment) + val event1_1 = MetadataEvent(key1, Option(MetadataValue("value1")), moment) + val event1_2 = MetadataEvent(key1, Option(MetadataValue("value2")), moment) + val event2_1 = MetadataEvent(key2, Option(MetadataValue("value1")), moment) + val event3_1 = MetadataEvent(key3, Option(MetadataValue("value3")), moment) + val event3_2 = MetadataEvent(key3, None, moment) - "MetadataServiceActor" should { "Store values for different keys" in { val putAction1 = PutMetadataAction(event1_1) val putAction2 = PutMetadataAction(event1_2) @@ -72,5 +72,25 @@ class MetadataServiceActorSpec extends ServicesSpec("Metadata") { } yield ()).futureValue } + + "store and retrieve null values" in { + val metadataWorkflowId = WorkflowId.randomId() + val metadataKey = MetadataKey(metadataWorkflowId, None, "nullTestKey") + val metadataValue = MetadataValue(null) + val metadataEvent = MetadataEvent(metadataKey, Option(metadataValue), moment) + + val putMetadataAction = PutMetadataAction(metadataEvent) + + val metadataQuery = MetadataQuery.forKey(metadataKey) + + (for { + putResponse <- (actor ? putMetadataAction).mapTo[MetadataServiceResponse] + _ = putResponse shouldBe MetadataPutAcknowledgement(putMetadataAction) + + queryResponse <- (actor ? GetMetadataQueryAction(metadataQuery)).mapTo[MetadataServiceResponse] + _ = queryResponse shouldBe MetadataLookupResponse(metadataQuery, Seq(metadataEvent)) + } yield ()).futureValue + } + } } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala index 00b5bfc96..7026c8cc2 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendLifecycleActorFactory.scala @@ -11,7 +11,6 @@ import wdl4s.expression.WdlStandardLibraryFunctions case class JesBackendLifecycleActorFactory(name: String, configurationDescriptor: BackendConfigurationDescriptor) extends StandardLifecycleActorFactory { - import JesBackendLifecycleActorFactory._ override def initializationActorClass: Class[_ <: StandardInitializationActor] = classOf[JesInitializationActor] @@ -45,14 +44,14 @@ case class JesBackendLifecycleActorFactory(name: String, configurationDescriptor initializationDataOption) } - override def runtimeAttributeDefinitions(initializationDataOption: Option[BackendInitializationData]): - Set[RuntimeAttributeDefinition] = staticRuntimeAttributeDefinitions - override def expressionLanguageFunctions(workflowDescriptor: BackendWorkflowDescriptor, jobKey: BackendJobDescriptorKey, - initializationData: Option[BackendInitializationData]): WdlStandardLibraryFunctions = { + initializationDataOption: Option[BackendInitializationData] + ): WdlStandardLibraryFunctions = { + val initializationData = BackendInitializationData. + as[JesBackendInitializationData](initializationDataOption) - val jesCallPaths = initializationData.toJes.get.workflowPaths.toJobPaths(jobKey, workflowDescriptor) + val jesCallPaths = initializationData.workflowPaths.toJobPaths(jobKey, workflowDescriptor) new JesExpressionFunctions(List(jesCallPaths.gcsPathBuilder), jesCallPaths.callContext) } @@ -60,15 +59,3 @@ case class JesBackendLifecycleActorFactory(name: String, configurationDescriptor override lazy val fileHashingFunction: Option[FileHashingFunction] = Option(FileHashingFunction(JesBackendFileHashing.getCrc32c)) } - -object JesBackendLifecycleActorFactory { - implicit class Jessify(val genericInitializationData: Option[BackendInitializationData]) { - // This leaves the result in an `Option` as finalization will be called even if initialization has failed, and if - // initialization fails there won't be any initialization data. The various `.get`s that occur below are in instances - // where the workflow has successfully gotten past initialization and the JES initialization data is defined. - def toJes: Option[JesBackendInitializationData] = genericInitializationData collectFirst { case d: JesBackendInitializationData => d } - } - - val staticRuntimeAttributeDefinitions: Set[RuntimeAttributeDefinition] = - JesRuntimeAttributes.runtimeAttributesBuilder.definitions.toSet -} diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala index a878974a8..dbd3bd392 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala @@ -86,8 +86,9 @@ object JesRuntimeAttributes { val memory: MemorySize = RuntimeAttributesValidation.extract(memoryValidation, validatedRuntimeAttributes) val disks: Seq[JesAttachedDisk] = RuntimeAttributesValidation.extract(disksValidation, validatedRuntimeAttributes) val docker: String = RuntimeAttributesValidation.extract(dockerValidation, validatedRuntimeAttributes) - val failOnStderr = RuntimeAttributesValidation.extract(FailOnStderrValidation.default, validatedRuntimeAttributes) - val continueOnReturnCode = + val failOnStderr: Boolean = + RuntimeAttributesValidation.extract(FailOnStderrValidation.default, validatedRuntimeAttributes) + val continueOnReturnCode: ContinueOnReturnCode = RuntimeAttributesValidation.extract(ContinueOnReturnCodeValidation.default, validatedRuntimeAttributes) val noAddress: Boolean = RuntimeAttributesValidation.extract(noAddressValidation, validatedRuntimeAttributes) diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala index 40fc7f44c..f331f73a8 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala @@ -19,6 +19,7 @@ import cromwell.core._ import cromwell.core.logging.JobLogger import cromwell.core.path.PathImplicits._ import cromwell.filesystems.gcs.auth.GoogleAuthMode.NoAuthMode +import cromwell.filesystems.gcs.auth.GoogleAuthModeSpec import cromwell.filesystems.gcs.{GcsPathBuilder, GcsPathBuilderFactory} import cromwell.util.SampleWdl import org.scalatest._ @@ -37,7 +38,10 @@ import scala.util.{Success, Try} class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackendJobExecutionActorSpec") with FlatSpecLike with Matchers with ImplicitSender with Mockito with BackendSpec { - val mockPathBuilder: GcsPathBuilder = GcsPathBuilderFactory(NoAuthMode).withOptions(mock[WorkflowOptions]) + lazy val mockPathBuilder: GcsPathBuilder = { + GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials() + GcsPathBuilderFactory(NoAuthMode).withOptions(mock[WorkflowOptions]) + } import JesTestConfig._ @@ -68,9 +72,9 @@ class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackend val NoOptions = WorkflowOptions(JsObject(Map.empty[String, JsValue])) - val TestableCallContext = CallContext(mockPathBuilder.build("gs://root").get, "out", "err") + lazy val TestableCallContext = CallContext(mockPathBuilder.build("gs://root").get, "out", "err") - val TestableJesExpressionFunctions: JesExpressionFunctions = { + lazy val TestableJesExpressionFunctions: JesExpressionFunctions = { new JesExpressionFunctions(List(mockPathBuilder), TestableCallContext) } @@ -676,6 +680,7 @@ class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackend private def makeRuntimeAttributes(job: TaskCall) = { val evaluatedAttributes = RuntimeAttributeDefinition.evaluateRuntimeAttributes(job.task.runtimeAttributes, TestableJesExpressionFunctions, Map.empty) - RuntimeAttributeDefinition.addDefaultsToAttributes(JesBackendLifecycleActorFactory.staticRuntimeAttributeDefinitions, NoOptions)(evaluatedAttributes.get) // Fine to throw the exception if this "get" fails. This is a test after all! + RuntimeAttributeDefinition.addDefaultsToAttributes( + JesRuntimeAttributes.runtimeAttributesBuilder.definitions.toSet, NoOptions)(evaluatedAttributes.get) } } diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesCallPathsSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesCallPathsSpec.scala index 7682ffb7b..1abf3f22f 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesCallPathsSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesCallPathsSpec.scala @@ -3,6 +3,7 @@ package cromwell.backend.impl.jes import cromwell.backend.BackendSpec import cromwell.core.TestKitSuite import cromwell.core.path.PathImplicits._ +import cromwell.filesystems.gcs.auth.GoogleAuthModeSpec import cromwell.util.SampleWdl import org.scalatest.{FlatSpecLike, Matchers} import org.specs2.mock.Mockito @@ -15,6 +16,8 @@ class JesCallPathsSpec extends TestKitSuite with FlatSpecLike with Matchers with behavior of "JesCallPaths" it should "map the correct filenames" in { + GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials() + val workflowDescriptor = buildWorkflowDescriptor(SampleWdl.HelloWorld.wdlSource()) val jobDescriptorKey = firstJobDescriptorKey(workflowDescriptor) val jesConfiguration = new JesConfiguration(JesBackendConfigurationDescriptor) @@ -28,6 +31,8 @@ class JesCallPathsSpec extends TestKitSuite with FlatSpecLike with Matchers with } it should "map the correct paths" in { + GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials() + val workflowDescriptor = buildWorkflowDescriptor(SampleWdl.HelloWorld.wdlSource()) val jobDescriptorKey = firstJobDescriptorKey(workflowDescriptor) val jesConfiguration = new JesConfiguration(JesBackendConfigurationDescriptor) @@ -44,6 +49,8 @@ class JesCallPathsSpec extends TestKitSuite with FlatSpecLike with Matchers with } it should "map the correct call context" in { + GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials() + val workflowDescriptor = buildWorkflowDescriptor(SampleWdl.HelloWorld.wdlSource()) val jobDescriptorKey = firstJobDescriptorKey(workflowDescriptor) val jesConfiguration = new JesConfiguration(JesBackendConfigurationDescriptor) diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala index 44688edd8..844fe73d8 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesInitializationActorSpec.scala @@ -11,7 +11,7 @@ import cromwell.core.Tags.IntegrationTest import cromwell.core.logging.LoggingTest._ import cromwell.core.{TestKitSuite, WorkflowOptions} import cromwell.filesystems.gcs.GoogleConfiguration -import cromwell.filesystems.gcs.auth.{RefreshTokenMode, SimpleClientSecrets} +import cromwell.filesystems.gcs.auth.{GoogleAuthModeSpec, RefreshTokenMode, SimpleClientSecrets} import cromwell.util.{EncryptionSpec, SampleWdl} import org.scalatest.{FlatSpecLike, Matchers} import org.specs2.mock.Mockito @@ -145,6 +145,8 @@ class JesInitializationActorSpec extends TestKitSuite("JesInitializationActorSpe behavior of "JesInitializationActor" it should "log a warning message when there are unsupported runtime attributes" taggedAs IntegrationTest in { + GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials() + within(Timeout) { val workflowDescriptor = buildWorkflowDescriptor(HelloWorld, runtime = """runtime { docker: "ubuntu/latest" test: true }""") diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala index 43b62287c..d8ddd68e2 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala @@ -190,7 +190,8 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { } private def assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes: Map[String, WdlValue], expectedRuntimeAttributes: JesRuntimeAttributes, workflowOptions: WorkflowOptions = emptyWorkflowOptions): Unit = { - val withDefaults = RuntimeAttributeDefinition.addDefaultsToAttributes(JesBackendLifecycleActorFactory.staticRuntimeAttributeDefinitions, workflowOptions) _ + val withDefaults = RuntimeAttributeDefinition.addDefaultsToAttributes( + staticRuntimeAttributeDefinitions, workflowOptions) _ try { assert(JesRuntimeAttributes(withDefaults(runtimeAttributes), NOPLogger.NOP_LOGGER) == expectedRuntimeAttributes) } catch { @@ -200,7 +201,8 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { } private def assertJesRuntimeAttributesFailedCreation(runtimeAttributes: Map[String, WdlValue], exMsg: String, workflowOptions: WorkflowOptions = emptyWorkflowOptions): Unit = { - val withDefaults = RuntimeAttributeDefinition.addDefaultsToAttributes(JesBackendLifecycleActorFactory.staticRuntimeAttributeDefinitions, workflowOptions) _ + val withDefaults = RuntimeAttributeDefinition.addDefaultsToAttributes( + staticRuntimeAttributeDefinitions, workflowOptions) _ try { JesRuntimeAttributes(withDefaults(runtimeAttributes), NOPLogger.NOP_LOGGER) fail("A RuntimeException was expected.") @@ -211,4 +213,6 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { } private val emptyWorkflowOptions = WorkflowOptions.fromMap(Map.empty).get + private val staticRuntimeAttributeDefinitions: Set[RuntimeAttributeDefinition] = + JesRuntimeAttributes.runtimeAttributesBuilder.definitions.toSet } diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesWorkflowPathsSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesWorkflowPathsSpec.scala index 9826b4ef0..2b3794356 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesWorkflowPathsSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesWorkflowPathsSpec.scala @@ -3,6 +3,7 @@ package cromwell.backend.impl.jes import cromwell.backend.BackendSpec import cromwell.core.TestKitSuite import cromwell.core.path.PathImplicits._ +import cromwell.filesystems.gcs.auth.GoogleAuthModeSpec import cromwell.util.SampleWdl import org.scalatest.{FlatSpecLike, Matchers} import org.specs2.mock.Mockito @@ -14,6 +15,8 @@ class JesWorkflowPathsSpec extends TestKitSuite with FlatSpecLike with Matchers behavior of "JesWorkflowPaths" it should "map the correct paths" in { + GoogleAuthModeSpec.assumeHasApplicationDefaultCredentials() + val workflowDescriptor = buildWorkflowDescriptor(SampleWdl.HelloWorld.wdlSource()) val jesConfiguration = new JesConfiguration(JesBackendConfigurationDescriptor) diff --git a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala index 5296020c6..78060285d 100644 --- a/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala +++ b/supportedBackends/sfs/src/main/scala/cromwell/backend/impl/sfs/config/ConfigBackendLifecycleActorFactory.scala @@ -1,11 +1,10 @@ package cromwell.backend.impl.sfs.config import com.typesafe.config.Config +import cromwell.backend.BackendConfigurationDescriptor import cromwell.backend.callcaching.FileHashingActor.FileHashingFunction import cromwell.backend.impl.sfs.config.ConfigConstants._ import cromwell.backend.sfs._ -import cromwell.backend.standard.StandardInitializationData -import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, RuntimeAttributeDefinition} import cromwell.core.JobExecutionToken.JobExecutionTokenType import net.ceedubs.ficus.Ficus._ import org.slf4j.{Logger, LoggerFactory} @@ -33,14 +32,6 @@ class ConfigBackendLifecycleActorFactory(name: String, val configurationDescript classOf[DispatchedConfigAsyncJobExecutionActor] } - override def runtimeAttributeDefinitions(initializationDataOption: Option[BackendInitializationData]): - Set[RuntimeAttributeDefinition] = { - val initializationData = BackendInitializationData. - as[StandardInitializationData](initializationDataOption) - - initializationData.runtimeAttributesBuilder.definitions.toSet - } - override lazy val fileHashingFunction: Option[FileHashingFunction] = { logger.debug(hashingStrategy.toString) Option(FileHashingFunction(hashingStrategy.getHash)) From 24688163c61e5a56794c85841c7beaa37d3e379d Mon Sep 17 00:00:00 2001 From: Chris Llanwarne Date: Wed, 4 Jan 2017 17:53:39 -0500 Subject: [PATCH 06/84] Cromwell workflow ID as a JES label --- CHANGELOG.md | 2 +- README.md | 24 ++++++++++ project/Dependencies.scala | 4 +- .../main/scala/cromwell/backend/impl/jes/Run.scala | 23 +++++++++ .../cromwell/backend/impl/jes/labels/Label.scala | 54 ++++++++++++++++++++++ .../cromwell/backend/impl/jes/labels/Labels.scala | 17 +++++++ .../backend/impl/jes/labels/LabelSpec.scala | 45 ++++++++++++++++++ 7 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Label.scala create mode 100644 supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Labels.scala create mode 100644 supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/labels/LabelSpec.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index a12ac4c8c..acfd1f07a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 25 - +* Cromwell now applies default labels automatically to JES pipeline runs. ## 24 diff --git a/README.md b/README.md index 49c09df42..1d8d30a05 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ A [Workflow Management System](https://en.wikipedia.org/wiki/Workflow_management * [Refresh Token](#refresh-token) * [Docker](#docker) * [Monitoring](#monitoring) + * [Labelling Runs](#labelling-runs) + * [Label Escaping and Padding](#label-escaping-and-padding) * [Runtime Attributes](#runtime-attributes) * [Specifying Default Values](#specifying-default-values) * [continueOnReturnCode](#continueonreturncode) @@ -1383,6 +1385,28 @@ In order to monitor metrics (CPU, Memory, Disk usage...) about the VM during Cal The output of this script will be written to a `monitoring.log` file that will be available in the call gcs bucket when the call completes. This feature is meant to run a script in the background during long-running processes. It's possible that if the task is very short that the log file does not flush before de-localization happens and you will end up with a zero byte file. +### Labelling Runs + +Every call to JES from a Cromwell instance will be automatically labelled by Cromwell so that it can be queried about later. The current label set automatically applied is: + +| Key | Value | Example | Notes | +|-----|-------|---------|-------| +| cromwell-workflow-id | The Cromwell ID given to the root workflow (i.e. the ID returned by Cromwell on submission) | cromwell-d4b412c5-bf3d-4169-91b0-1b635ce47a26 | To fit the required [format](#label-escaping-and-padding), we prefix with 'cromwell-' | +| cromwell-workflow-name | The name of the root workflow | my-root-workflow | See [format](#label-escaping-and-padding). | +| cromwell-sub-workflow-name | The name of this job's sub-workflow | my-sub-workflow | Only if the task is called in a subworkflow, otherwise 'n-a'. See also [format](#label-escaping-and-padding). | +| wdl-task-name | The name of the WDL task | my-task | See [format](#label-escaping-and-padding). | +| wdl-call-name | The name of the WDL call of this job | my-call | Different from 'wdl-task-name' if it was called with an alias. See also [format](#label-escaping-and-padding). | + +#### Label Escaping and Padding + +To fit in with the Google schema for labels, label key and value strings must match the regex `[a-z]([-a-z0-9]*[a-z0-9])?` and be between 1 and 63 characters in length. For this reason, Cromwell will modify workflow/task/call names and custom labels as follows: + +- Any capital letters are lowercased. +- Any character which is not one of `[a-z]`, `[0-9]` or `-` will be replaced with `-`. +- If the start character does not match `[a-z]` then prefix with `x--` +- If the final character does not match `[a-z0-9]` then suffix with `--x` +- If the string is too long, only take the first 30 and last 30 characters and add `---` between them. + # Runtime Attributes Runtime attributes are used to customize tasks. Within a task one can specify runtime attributes to customize the environment for the call. diff --git a/project/Dependencies.scala b/project/Dependencies.scala index b06413d9d..f683b492b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { lazy val akkaV = "2.4.14" lazy val slickV = "3.1.1" lazy val googleClientApiV = "1.22.0" - lazy val googleGenomicsServicesApiV = "1.20.0" + lazy val googleGenomicsServicesApiV = "1.22.0" lazy val betterFilesV = "2.16.0" lazy val catsV = "0.7.2" @@ -80,7 +80,7 @@ object Dependencies { ) private val googleCloudDependencies = List( - "com.google.apis" % "google-api-services-genomics" % ("v1alpha2-rev14-" + googleGenomicsServicesApiV), + "com.google.apis" % "google-api-services-genomics" % ("v1alpha2-rev64-" + googleGenomicsServicesApiV), "com.google.cloud" % "google-cloud-nio" % "0.3.0" exclude("com.google.api.grpc", "grpc-google-common-protos") exclude("com.google.cloud.datastore", "datastore-v1-protos") diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/Run.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/Run.scala index 05078c7f2..290cb8f4e 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/Run.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/Run.scala @@ -8,6 +8,7 @@ import com.google.api.services.genomics.Genomics import com.google.api.services.genomics.model._ import cromwell.backend.BackendJobDescriptor import cromwell.backend.impl.jes.RunStatus.{Failed, Initializing, Running, Success} +import cromwell.backend.impl.jes.labels.Labels import cromwell.core.ExecutionEvent import cromwell.core.logging.JobLogger import org.slf4j.LoggerFactory @@ -59,6 +60,26 @@ object Run { resources.setDisks(disksWithoutMountPoint.asJava) } + lazy val labels: Labels = { + + val hasSubworkflow = workflow.workflow.equals(workflow.rootWorkflow) + + val subWorkflowName = if (hasSubworkflow) workflow.workflow.unqualifiedName else "n-a" + val subWorkflowLabels = Labels("cromwell-sub-workflow-name" -> subWorkflowName) + + val rootWorkflowLabels = Labels( + "cromwell-workflow-id" -> s"cromwell-${workflow.rootWorkflowId}", + "cromwell-workflow-name" -> workflow.rootWorkflow.unqualifiedName + ) + + val callLabels = Labels( + "wdl-call-name" -> jobDescriptor.call.unqualifiedName, + "wdl-task-name" -> jobDescriptor.call.task.name + ) + + rootWorkflowLabels ++ subWorkflowLabels ++ callLabels + } + def runPipeline: String = { val svcAccount = new ServiceAccount().setEmail(computeServiceAccount).setScopes(GenomicsScopes) val rpargs = new RunPipelineArgs().setProjectId(projectId).setServiceAccount(svcAccount).setResources(runtimePipelineResources) @@ -69,6 +90,8 @@ object Run { rpargs.setOutputs(jesParameters.collect({ case i: JesFileOutput => i.name -> i.toGoogleRunParameter }).toMap.asJava) logger.debug(s"Outputs:\n${stringifyMap(rpargs.getOutputs.asScala.toMap)}") + rpargs.setLabels(labels.asJesLabels) + val rpr = new RunPipelineRequest().setEphemeralPipeline(pipeline).setPipelineArgs(rpargs) val logging = new LoggingOptions() diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Label.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Label.scala new file mode 100644 index 000000000..0e7e83101 --- /dev/null +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Label.scala @@ -0,0 +1,54 @@ +package cromwell.backend.impl.jes.labels + +case class Label private[labels](key: String, value: String) + +object Label { + + // Yes, 63. Not a typo for 64. + // See 'labels' in https://cloud.google.com/genomics/reference/rpc/google.genomics.v1alpha2#google.genomics.v1alpha2.RunPipelineArgs + private val MAX_LABEL_LENGTH = 63 + + def validate(s: String) = "[a-z]([-a-z0-9]*[a-z0-9])?".r.pattern.matcher(s).matches && s.length <= MAX_LABEL_LENGTH + + /** + * Change to meet the constraint: + * - Must match the regex [a-z]([-a-z0-9]*[a-z0-9])? + * - Must be between 1 and MAX_LABEL_LENGTH characters total + */ + def safeName(mainText: String) = { + + if (validate(mainText)) { + mainText + } else { + def appendSafe(current: String, nextChar: Char): String = { + nextChar match { + case c if c.isLetterOrDigit || c == '-' => current + c.toLower + case _ => current + '-' + } + } + + val foldResult = mainText.toCharArray.foldLeft("")(appendSafe) + + val startsValid = foldResult.headOption.exists(_.isLetter) + val endsValid = foldResult.lastOption.exists(_.isLetterOrDigit) + + val validStart = if (startsValid) foldResult else "x--" + foldResult + val validStartAndEnd = if (endsValid) validStart else validStart + "--x" + + val length = validStartAndEnd.length + val tooLong = length > MAX_LABEL_LENGTH + + if (tooLong) { + val middleSeparator = "---" + val subSectionLength = (MAX_LABEL_LENGTH - middleSeparator.length) / 2 + validStartAndEnd.substring(0, subSectionLength) + middleSeparator + validStartAndEnd.substring(length - subSectionLength, length) + } else { + validStartAndEnd + } + } + } + + def safeLabel(key: String, value: String): Label = { + Label(safeName(key), safeName(value)) + } +} diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Labels.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Labels.scala new file mode 100644 index 000000000..6932e1fc8 --- /dev/null +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Labels.scala @@ -0,0 +1,17 @@ +package cromwell.backend.impl.jes.labels + +import scala.collection.JavaConverters._ + +case class Labels(value: Vector[Label]) { + + def asJesLabels = (value map { label => label.key -> label.value }).toMap.asJava + + def ++(that: Labels) = Labels(value ++ that.value) +} + +object Labels { + def apply(values: (String, String)*): Labels = { + val kvps: Seq[(String, String)] = values.toSeq + Labels((kvps map { case (k, v) => Label.safeLabel(k, v) }).to[Vector]) + } +} diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/labels/LabelSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/labels/LabelSpec.scala new file mode 100644 index 000000000..41c19140b --- /dev/null +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/labels/LabelSpec.scala @@ -0,0 +1,45 @@ +package cromwell.backend.impl.jes.labels + +import org.scalatest.{FlatSpec, Matchers} + +class LabelSpec extends FlatSpec with Matchers { + + behavior of "Labels" + + /** + * In the format 'to validate', 'expected result' + */ + val goodLabelStrings = List( + "cromwell-root-workflow-id", + "cromwell-11f2468c-39d6-4be3-85c8-32735c01e66b", + "just-the-right-length-just-the-right-length-just-the-right-leng" + ) + + val badLabelConversions = List( + "11f2468c-39d6-4be3-85c8-32735c01e66b" -> "x--11f2468c-39d6-4be3-85c8-32735c01e66b", + "0-cromwell-root-workflow-id" -> "x--0-cromwell-root-workflow-id", + "" -> "x----x", + "cromwell-root-workflow-id-" -> "cromwell-root-workflow-id---x", + "0-cromwell-root-workflow-id-" -> "x--0-cromwell-root-workflow-id---x", + "Cromwell-root-workflow-id" -> "cromwell-root-workflow-id", + "cromwell_root_workflow_id" -> "cromwell-root-workflow-id", + "too-long-too-long-too-long-too-long-too-long-too-long-too-long-t" -> "too-long-too-long-too-long-too---g-too-long-too-long-too-long-t", + "0-too-long-and-invalid-too-long-and-invalid-too-long-and-invali+" -> "x--0-too-long-and-invalid-too----nvalid-too-long-and-invali---x" + ) + + goodLabelStrings foreach { label => + it should s"validate the good label string '$label'" in { + Label.validate(label) should be(true) + } + } + + badLabelConversions foreach { case (label: String, conversion: String) => + it should s"not validate the bad label string '$label'" in { + Label.validate(label) should be(false) + } + + it should s"convert the bad label string '$label' into the safe label string '$conversion'" in { + Label.safeName(label) should be(conversion) + } + } +} From a7231aee8727c6ebd7c963b52af6c59bd9a1320d Mon Sep 17 00:00:00 2001 From: Joel Thibault Date: Mon, 9 Jan 2017 17:33:31 -0500 Subject: [PATCH 07/84] consistent commenting style --- core/src/main/resources/reference.conf | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 60e3903de..6b54f966d 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -125,18 +125,18 @@ google { name = "application-default" scheme = "application_default" }, -// { -// name = "user-via-refresh" -// scheme = "refresh_token" -// client-id = "secret_id" -// client-secret = "secret_secret" -// }, -// { -// name = "service-account" -// scheme = "service_account" -// service-account-id = "my-service-account" -// pem-file = "/path/to/file.pem" -// } + #{ + # name = "user-via-refresh" + # scheme = "refresh_token" + # client-id = "secret_id" + # client-secret = "secret_secret" + #}, + #{ + # name = "service-account" + # scheme = "service_account" + # service-account-id = "my-service-account" + # pem-file = "/path/to/file.pem" + #} ] } From 45818562eed5b7c2aa9161ffc58962686e11cf1b Mon Sep 17 00:00:00 2001 From: Jeff Gentry Date: Mon, 9 Jan 2017 20:46:19 -0500 Subject: [PATCH 08/84] Allow default zones to be set in the config. Closes #1795 (#1797) * Allow default zones to be set in the config. Closes #1795 --- CHANGELOG.md | 1 + README.md | 2 +- core/src/main/resources/reference.conf | 3 +++ .../cromwell/backend/impl/jes/JesAttributes.scala | 28 +++++++++++++++------- .../impl/jes/JesBackendInitializationData.scala | 5 ++-- .../backend/impl/jes/JesConfiguration.scala | 1 + .../backend/impl/jes/JesInitializationActor.scala | 8 +++---- .../backend/impl/jes/JesRuntimeAttributes.scala | 17 ++++++------- .../jes/JesAsyncBackendJobExecutionActorSpec.scala | 6 +++-- .../backend/impl/jes/JesAttributesSpec.scala | 5 ++++ .../backend/impl/jes/JesConfigurationSpec.scala | 7 +++++- .../impl/jes/JesRuntimeAttributesSpec.scala | 25 ++++++++++++++----- 12 files changed, 76 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acfd1f07a..f96e88a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 25 * Cromwell now applies default labels automatically to JES pipeline runs. +* Added ability to override the default zone(s) used by JES via the config structure by setting `genomics.default-zones` in the JES configuration ## 24 diff --git a/README.md b/README.md index 1d8d30a05..63f2701fb 100644 --- a/README.md +++ b/README.md @@ -1610,7 +1610,7 @@ runtime { } ``` -Defaults to "us-central1-b" +Defaults to the configuration setting `genomics.default-zones` in the JES configuration block which in turn defaults to using `us-central1-b` ## docker diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 6b54f966d..765042ea7 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -352,6 +352,9 @@ backend { # # Pipelines and manipulate auth JSONs. # auth = "application-default" # + # # Specifies the zone(s) to use for JES jobs unless overridden by a task's runtime attributes + # default-zones = ["us-central1-b"] + # # // alternative service account to use on the launched compute instance # // NOTE: If combined with service account authorization, both that serivce account and this service account # // must be able to read and write to the 'root' GCS path diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala index 14ca8040e..dff6a755c 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala @@ -2,14 +2,16 @@ package cromwell.backend.impl.jes import java.net.{URI, URL} +import cats.data.NonEmptyList import cats.data.Validated._ import cats.syntax.cartesian._ +import cats.syntax.validated._ import com.typesafe.config.{Config, ConfigValue} import cromwell.backend.impl.jes.authentication.JesAuths import cromwell.filesystems.gcs.GoogleConfiguration -import lenthall.validation.Validation._ import lenthall.exception.MessageAggregation import lenthall.validation.ErrorOr._ +import lenthall.validation.Validation._ import net.ceedubs.ficus.Ficus._ import net.ceedubs.ficus.readers.{StringReader, ValueReader} import org.slf4j.LoggerFactory @@ -22,7 +24,8 @@ case class JesAttributes(project: String, executionBucket: String, endpointUrl: URL, maxPollingInterval: Int, - qps: Int) + qps: Int, + defaultZones: NonEmptyList[String]) object JesAttributes { lazy val Logger = LoggerFactory.getLogger("JesAttributes") @@ -40,7 +43,8 @@ object JesAttributes { "genomics.auth", "genomics.endpoint-url", "filesystems.gcs.auth", - "genomics-api-queries-per-100-seconds" + "genomics-api-queries-per-100-seconds", + "genomics.default-zones" ) private val context = "Jes" @@ -58,14 +62,14 @@ object JesAttributes { val computeServiceAccount: String = backendConfig.as[Option[String]]("genomics.compute-service-account").getOrElse("default") val genomicsAuthName: ErrorOr[String] = validate { backendConfig.as[String]("genomics.auth") } val gcsFilesystemAuthName: ErrorOr[String] = validate { backendConfig.as[String]("filesystems.gcs.auth") } - val qps = backendConfig.as[Option[Int]]("genomics-api-queries-per-100-seconds").getOrElse(GenomicsApiDefaultQps) / 100 + val defaultZones = defaultZonesFromConfig(backendConfig) - (project |@| executionBucket |@| endpointUrl |@| genomicsAuthName |@| gcsFilesystemAuthName) map { - (_, _, _, _, _) - } flatMap { case (p, b, u, genomicsName, gcsName) => + (project |@| executionBucket |@| endpointUrl |@| genomicsAuthName |@| gcsFilesystemAuthName |@| defaultZones) map { + (_, _, _, _, _, _) + } flatMap { case (p, b, u, genomicsName, gcsName, d) => (googleConfig.auth(genomicsName) |@| googleConfig.auth(gcsName)) map { case (genomicsAuth, gcsAuth) => - JesAttributes(p, computeServiceAccount, JesAuths(genomicsAuth, gcsAuth), b, u, maxPollingInterval, qps) + JesAttributes(p, computeServiceAccount, JesAuths(genomicsAuth, gcsAuth), b, u, maxPollingInterval, qps, d) } } match { case Valid(r) => r @@ -76,4 +80,12 @@ object JesAttributes { } } } + + def defaultZonesFromConfig(config: Config): ErrorOr[NonEmptyList[String]] = { + val zones = config.as[Option[List[String]]]("genomics.default-zones").getOrElse(List("us-central1-b")) + zones match { + case x :: xs => NonEmptyList(x, xs).validNel + case _ => "genomics.default-zones was set but no values were provided".invalidNel + } + } } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala index 0d3d07c31..7662b79e6 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesBackendInitializationData.scala @@ -1,11 +1,12 @@ package cromwell.backend.impl.jes import com.google.api.services.genomics.Genomics -import cromwell.backend.standard.StandardInitializationData +import cromwell.backend.standard.{StandardInitializationData, StandardValidatedRuntimeAttributesBuilder} case class JesBackendInitializationData ( override val workflowPaths: JesWorkflowPaths, + override val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder, jesConfiguration: JesConfiguration, genomics: Genomics -) extends StandardInitializationData(workflowPaths, JesRuntimeAttributes.runtimeAttributesBuilder) +) extends StandardInitializationData(workflowPaths, runtimeAttributesBuilder) diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesConfiguration.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesConfiguration.scala index 20ec8130d..2c1e82f92 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesConfiguration.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesConfiguration.scala @@ -34,4 +34,5 @@ class JesConfiguration(val configurationDescriptor: BackendConfigurationDescript val dockerCredentials = DockerConfiguration.build(configurationDescriptor.backendConfig).dockerCredentials map JesDockerCredentials.apply val needAuthFileUpload = jesAuths.gcs.requiresAuthFile || dockerCredentials.isDefined val qps = jesAttributes.qps + val defaultZones = jesAttributes.defaultZones } diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala index 20d49fa9a..3ebaa570d 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesInitializationActor.scala @@ -44,11 +44,11 @@ class JesInitializationActor(jesParams: JesInitializationActorParams) override val standardParams: StandardInitializationActorParams = jesParams - override lazy val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder = - JesRuntimeAttributes.runtimeAttributesBuilder - private val jesConfiguration = jesParams.jesConfiguration + override lazy val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder = + JesRuntimeAttributes.runtimeAttributesBuilder(jesConfiguration) + private[jes] lazy val refreshTokenAuth: Option[JesAuthInformation] = { for { clientSecrets <- List(jesConfiguration.jesAttributes.auths.gcs) collectFirst { case s: ClientSecrets => s } @@ -70,7 +70,7 @@ class JesInitializationActor(jesParams: JesInitializationActorParams) workflowPaths = new JesWorkflowPaths(workflowDescriptor, jesConfiguration)(context.system) _ <- if (jesConfiguration.needAuthFileUpload) writeAuthenticationFile(workflowPaths) else Future.successful(()) _ = publishWorkflowRoot(workflowPaths.workflowRoot.toString) - } yield Option(JesBackendInitializationData(workflowPaths, jesConfiguration, genomics)) + } yield Option(JesBackendInitializationData(workflowPaths, runtimeAttributesBuilder, jesConfiguration, genomics)) } private def writeAuthenticationFile(workflowPath: JesWorkflowPaths): Future[Unit] = { diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala index dbd3bd392..b62aa445b 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesRuntimeAttributes.scala @@ -1,5 +1,6 @@ package cromwell.backend.impl.jes +import cats.data.NonEmptyList import cats.data.Validated._ import cats.syntax.cartesian._ import cats.syntax.validated._ @@ -27,7 +28,6 @@ object JesRuntimeAttributes { private val MemoryDefaultValue = "2 GB" val ZonesKey = "zones" - private val ZoneDefaultValue = "us-central1-b" val PreemptibleKey = "preemptible" private val PreemptibleDefaultValue = 0 @@ -46,8 +46,8 @@ object JesRuntimeAttributes { private val disksValidation: RuntimeAttributesValidation[Seq[JesAttachedDisk]] = DisksValidation.withDefault(WdlString(DisksDefaultValue)) - private val zonesValidation: RuntimeAttributesValidation[Vector[String]] = - ZonesValidation.withDefault(WdlString(ZoneDefaultValue)) + private def zonesValidation(defaultZones: NonEmptyList[String]): RuntimeAttributesValidation[Vector[String]] = + ZonesValidation.withDefault(WdlString(defaultZones.toList.mkString(" "))) private val preemptibleValidation: RuntimeAttributesValidation[Int] = new IntRuntimeAttributesValidation(JesRuntimeAttributes.PreemptibleKey) @@ -66,11 +66,11 @@ object JesRuntimeAttributes { private val dockerValidation: RuntimeAttributesValidation[String] = DockerValidation.instance - val runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder = + def runtimeAttributesBuilder(jesConfiguration: JesConfiguration): StandardValidatedRuntimeAttributesBuilder = StandardValidatedRuntimeAttributesBuilder.default.withValidation( cpuValidation, disksValidation, - zonesValidation, + zonesValidation(jesConfiguration.defaultZones), preemptibleValidation, memoryValidation, bootDiskSizeValidation, @@ -80,7 +80,7 @@ object JesRuntimeAttributes { def apply(validatedRuntimeAttributes: ValidatedRuntimeAttributes): JesRuntimeAttributes = { val cpu: Int = RuntimeAttributesValidation.extract(cpuValidation, validatedRuntimeAttributes) - val zones: Vector[String] = RuntimeAttributesValidation.extract(zonesValidation, validatedRuntimeAttributes) + val zones: Vector[String] = RuntimeAttributesValidation.extract(ZonesValidation, validatedRuntimeAttributes) val preemptible: Int = RuntimeAttributesValidation.extract(preemptibleValidation, validatedRuntimeAttributes) val bootDiskSize: Int = RuntimeAttributesValidation.extract(bootDiskSizeValidation, validatedRuntimeAttributes) val memory: MemorySize = RuntimeAttributesValidation.extract(memoryValidation, validatedRuntimeAttributes) @@ -107,8 +107,9 @@ object JesRuntimeAttributes { } // NOTE: Currently only used by test specs - private[jes] def apply(attrs: Map[String, WdlValue], logger: Logger): JesRuntimeAttributes = { - val runtimeAttributesBuilder = JesRuntimeAttributes.runtimeAttributesBuilder + private[jes] def apply(attrs: Map[String, WdlValue], logger: Logger, + jesConfiguration: JesConfiguration): JesRuntimeAttributes = { + val runtimeAttributesBuilder = JesRuntimeAttributes.runtimeAttributesBuilder(jesConfiguration) val validatedRuntimeAttributes = runtimeAttributesBuilder.build(attrs, logger) apply(validatedRuntimeAttributes) } diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala index f331f73a8..605cc7201 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAsyncBackendJobExecutionActorSpec.scala @@ -80,7 +80,8 @@ class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackend private def buildInitializationData(jobDescriptor: BackendJobDescriptor, configuration: JesConfiguration) = { val workflowPaths = JesWorkflowPaths(jobDescriptor.workflowDescriptor, configuration)(system) - JesBackendInitializationData(workflowPaths, configuration, null) + val runtimeAttributesBuilder = JesRuntimeAttributes.runtimeAttributesBuilder(configuration) + JesBackendInitializationData(workflowPaths, runtimeAttributesBuilder, configuration, null) } class TestableJesJobExecutionActor(params: StandardAsyncExecutionActorParams, functions: JesExpressionFunctions) @@ -114,6 +115,7 @@ class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackend } private val jesConfiguration = new JesConfiguration(JesBackendConfigurationDescriptor) + private val runtimeAttributesBuilder = JesRuntimeAttributes.runtimeAttributesBuilder(jesConfiguration) private val workingDisk = JesWorkingDisk(DiskType.SSD, 200) val DockerAndDiskRuntime: String = @@ -681,6 +683,6 @@ class JesAsyncBackendJobExecutionActorSpec extends TestKitSuite("JesAsyncBackend private def makeRuntimeAttributes(job: TaskCall) = { val evaluatedAttributes = RuntimeAttributeDefinition.evaluateRuntimeAttributes(job.task.runtimeAttributes, TestableJesExpressionFunctions, Map.empty) RuntimeAttributeDefinition.addDefaultsToAttributes( - JesRuntimeAttributes.runtimeAttributesBuilder.definitions.toSet, NoOptions)(evaluatedAttributes.get) + runtimeAttributesBuilder.definitions.toSet, NoOptions)(evaluatedAttributes.get) } } diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAttributesSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAttributesSpec.scala index 00eeb24fd..5d9a5724e 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAttributesSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesAttributesSpec.scala @@ -24,6 +24,7 @@ class JesAttributesSpec extends FlatSpec with Matchers { jesAttributes.executionBucket should be("gs://myBucket") jesAttributes.maxPollingInterval should be(600) jesAttributes.computeServiceAccount should be("default") + jesAttributes.defaultZones.toList should be (List("us-central1-a")) } it should "parse correct preemptible config" taggedAs IntegrationTest in { @@ -35,6 +36,7 @@ class JesAttributesSpec extends FlatSpec with Matchers { jesAttributes.project should be("myProject") jesAttributes.executionBucket should be("gs://myBucket") jesAttributes.maxPollingInterval should be(600) + jesAttributes.defaultZones.toList should be (List("us-central1-a")) } it should "parse compute service account" taggedAs IntegrationTest in { @@ -52,6 +54,7 @@ class JesAttributesSpec extends FlatSpec with Matchers { |{ | genomics { | endpoint-url = "myEndpoint" + | default-zones = [] | } |} """.stripMargin) @@ -67,6 +70,7 @@ class JesAttributesSpec extends FlatSpec with Matchers { errorsList should contain("No configuration setting found for key 'genomics.auth'") errorsList should contain("No configuration setting found for key 'filesystems'") errorsList should contain("URI is not absolute") + errorsList should contain("genomics.default-zones was set but no values were provided") } def configString(preemptible: String = "", genomics: String = ""): String = @@ -82,6 +86,7 @@ class JesAttributesSpec extends FlatSpec with Matchers { | auth = "application-default" | $genomics | endpoint-url = "http://myEndpoint" + | default-zones = ["us-central1-a"] | } | | filesystems = { diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesConfigurationSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesConfigurationSpec.scala index 46068343c..f94991670 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesConfigurationSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesConfigurationSpec.scala @@ -1,7 +1,7 @@ package cromwell.backend.impl.jes import better.files.File -import com.typesafe.config.{ConfigValueFactory, ConfigFactory} +import com.typesafe.config.{ConfigFactory, ConfigValueFactory} import cromwell.backend.BackendConfigurationDescriptor import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} @@ -63,6 +63,7 @@ class JesConfigurationSpec extends FlatSpec with Matchers with TableDrivenProper | auth = "application-default" | // Endpoint for APIs, no reason to change this unless directed by Google. | endpoint-url = "https://genomics.googleapis.com/" + | default-zones = ["us-central1-a", "us-central1-b"] | } | | dockerhub { @@ -103,6 +104,10 @@ class JesConfigurationSpec extends FlatSpec with Matchers with TableDrivenProper new JesConfiguration(BackendConfigurationDescriptor(backendConfig, globalConfig)).root shouldBe "gs://my-cromwell-workflows-bucket" } + it should "have the correct default zones" in { + new JesConfiguration(BackendConfigurationDescriptor(backendConfig, globalConfig)).defaultZones.toList shouldBe List("us-central1-a", "us-central1-b") + } + it should "have correct docker" in { val dockerConf = new JesConfiguration(BackendConfigurationDescriptor(backendConfig, globalConfig)).dockerCredentials dockerConf shouldBe defined diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala index d8ddd68e2..cb18aa907 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala +++ b/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/JesRuntimeAttributesSpec.scala @@ -1,5 +1,6 @@ package cromwell.backend.impl.jes +import cats.data.NonEmptyList import cromwell.backend.impl.jes.io.{DiskType, JesAttachedDisk, JesWorkingDisk} import cromwell.backend.validation.ContinueOnReturnCodeSet import cromwell.backend.{MemorySize, RuntimeAttributeDefinition} @@ -20,8 +21,9 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { ))) } - val expectedDefaults = new JesRuntimeAttributes(1, Vector("us-central1-b"), 0, 10, MemorySize(2, MemoryUnit.GB), - Seq(JesWorkingDisk(DiskType.SSD, 10)), "ubuntu:latest", false, ContinueOnReturnCodeSet(Set(0)), false) + val expectedDefaults = new JesRuntimeAttributes(1, Vector("us-central1-b", "us-central1-a"), 0, 10, + MemorySize(2, MemoryUnit.GB), Seq(JesWorkingDisk(DiskType.SSD, 10)), "ubuntu:latest", false, + ContinueOnReturnCodeSet(Set(0)), false) "JesRuntimeAttributes" should { @@ -189,11 +191,16 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { } } - private def assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes: Map[String, WdlValue], expectedRuntimeAttributes: JesRuntimeAttributes, workflowOptions: WorkflowOptions = emptyWorkflowOptions): Unit = { + private def assertJesRuntimeAttributesSuccessfulCreation(runtimeAttributes: Map[String, WdlValue], + expectedRuntimeAttributes: JesRuntimeAttributes, + workflowOptions: WorkflowOptions = emptyWorkflowOptions, + defaultZones: NonEmptyList[String] = defaultZones): Unit = { val withDefaults = RuntimeAttributeDefinition.addDefaultsToAttributes( staticRuntimeAttributeDefinitions, workflowOptions) _ try { - assert(JesRuntimeAttributes(withDefaults(runtimeAttributes), NOPLogger.NOP_LOGGER) == expectedRuntimeAttributes) + val actualRuntimeAttributes = + JesRuntimeAttributes(withDefaults(runtimeAttributes), NOPLogger.NOP_LOGGER, jesConfiguration) + assert(actualRuntimeAttributes == expectedRuntimeAttributes) } catch { case ex: RuntimeException => fail(s"Exception was not expected but received: ${ex.getMessage}") } @@ -204,7 +211,7 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { val withDefaults = RuntimeAttributeDefinition.addDefaultsToAttributes( staticRuntimeAttributeDefinitions, workflowOptions) _ try { - JesRuntimeAttributes(withDefaults(runtimeAttributes), NOPLogger.NOP_LOGGER) + JesRuntimeAttributes(withDefaults(runtimeAttributes), NOPLogger.NOP_LOGGER, jesConfiguration) fail("A RuntimeException was expected.") } catch { case ex: RuntimeException => assert(ex.getMessage.contains(exMsg)) @@ -213,6 +220,12 @@ class JesRuntimeAttributesSpec extends WordSpecLike with Matchers with Mockito { } private val emptyWorkflowOptions = WorkflowOptions.fromMap(Map.empty).get + private val defaultZones = NonEmptyList.of("us-central1-b", "us-central1-a") + private val jesConfiguration = { + val config = mock[JesConfiguration] + config.defaultZones returns defaultZones + config + } private val staticRuntimeAttributeDefinitions: Set[RuntimeAttributeDefinition] = - JesRuntimeAttributes.runtimeAttributesBuilder.definitions.toSet + JesRuntimeAttributes.runtimeAttributesBuilder(jesConfiguration).definitions.toSet } From 7d8ccdc288e178dd75066cfd88c9b5b0faada98c Mon Sep 17 00:00:00 2001 From: Miguel Covarrubias Date: Fri, 6 Jan 2017 11:27:38 -0500 Subject: [PATCH 09/84] Add length(). Closes #1604 --- CHANGELOG.md | 2 ++ README.md | 1 + .../wdl/PureStandardLibraryFunctionsSpec.scala | 30 ---------------------- project/Dependencies.scala | 2 +- 4 files changed, 4 insertions(+), 31 deletions(-) delete mode 100644 backend/src/test/scala/cromwell/backend/wdl/PureStandardLibraryFunctionsSpec.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index f96e88a9a..53bd0ef36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Cromwell now applies default labels automatically to JES pipeline runs. * Added ability to override the default zone(s) used by JES via the config structure by setting `genomics.default-zones` in the JES configuration +* Added support for new WDL functions: + * `length: (Array[X]) => Integer` - report the length of the specified array ## 24 diff --git a/README.md b/README.md index 63f2701fb..69c6ccc8d 100644 --- a/README.md +++ b/README.md @@ -378,6 +378,7 @@ For many examples on how to use WDL see [the WDL site](https://github.com/broadi * [Array\[Array\[X\]\] transpose(Array\[Array\[X\]\])](https://github.com/broadinstitute/wdl/blob/develop/SPEC.md#arrayarrayx-transposearrayarrayx) * [Pair(X,Y) zip(X,Y)](https://github.com/broadinstitute/wdl/blob/develop/SPEC.md#pairxy-zipxy) * [Pair(X,Y) cross(X,Y)](https://github.com/broadinstitute/wdl/blob/develop/SPEC.md#pairxy-crossxy) + * [Integer length(Array\[X\])](https://github.com/broadinstitute/wdl/blob/develop/SPEC.md#integer-lengtharrayx) * [Data Types & Serialization](https://github.com/broadinstitute/wdl/blob/develop/SPEC.md#data-types--serialization) * [Serialization of Task Inputs](https://github.com/broadinstitute/wdl/blob/develop/SPEC.md#serialization-of-task-inputs) * [Primitive Types](https://github.com/broadinstitute/wdl/blob/develop/SPEC.md#primitive-types) diff --git a/backend/src/test/scala/cromwell/backend/wdl/PureStandardLibraryFunctionsSpec.scala b/backend/src/test/scala/cromwell/backend/wdl/PureStandardLibraryFunctionsSpec.scala deleted file mode 100644 index fb6507377..000000000 --- a/backend/src/test/scala/cromwell/backend/wdl/PureStandardLibraryFunctionsSpec.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cromwell.backend.wdl - -import org.scalatest.{FlatSpec, Matchers} -import wdl4s.expression.PureStandardLibraryFunctions -import wdl4s.types.{WdlArrayType, WdlIntegerType} -import wdl4s.values.{WdlArray, WdlInteger} - -import scala.util.Success - - -class PureStandardLibraryFunctionsSpec extends FlatSpec with Matchers { - - behavior of "transpose" - - it should "transpose a 2x3 into a 3x2" in { - val inArray = WdlArray(WdlArrayType(WdlArrayType(WdlIntegerType)), List( - WdlArray(WdlArrayType(WdlIntegerType), List(WdlInteger(1), WdlInteger(2), WdlInteger(3))), - WdlArray(WdlArrayType(WdlIntegerType), List(WdlInteger(4), WdlInteger(5), WdlInteger(6))) - )) - - val expectedResult = WdlArray(WdlArrayType(WdlArrayType(WdlIntegerType)), List( - WdlArray(WdlArrayType(WdlIntegerType), List(WdlInteger(1), WdlInteger(4))), - WdlArray(WdlArrayType(WdlIntegerType), List(WdlInteger(2), WdlInteger(5))), - WdlArray(WdlArrayType(WdlIntegerType), List(WdlInteger(3), WdlInteger(6))) - )) - - PureStandardLibraryFunctions.transpose(Seq(Success(inArray))) should be(Success(expectedResult)) - } - -} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f683b492b..31909fc5e 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,7 +2,7 @@ import sbt._ object Dependencies { lazy val lenthallV = "0.20" - lazy val wdl4sV = "0.8" + lazy val wdl4sV = "0.9-369e288-SNAP" lazy val sprayV = "1.3.3" /* spray-json is an independent project from the "spray suite" From 1930608fb0bc383ec21b9d059b38eaaed7fc0db0 Mon Sep 17 00:00:00 2001 From: Thib Date: Wed, 11 Jan 2017 09:11:46 -0500 Subject: [PATCH 10/84] Don't log stack traces for known failures Closes #1817 (#1824) * don't log stack traces for known failures --- .../backend/async/KnownJobFailureException.scala | 16 ++++++++++ .../standard/StandardAsyncExecutionActor.scala | 35 +++++++++------------- .../engine/workflow/WorkflowManagerActor.scala | 7 ++++- .../scala/cromwell/SimpleWorkflowActorSpec.scala | 2 +- .../SharedFileSystemJobExecutionActorSpec.scala | 3 +- 5 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala diff --git a/backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala b/backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala new file mode 100644 index 000000000..4828678b8 --- /dev/null +++ b/backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala @@ -0,0 +1,16 @@ +package cromwell.backend.async + +import java.nio.file.Path + +sealed abstract class KnownJobFailureException extends Exception { + def stderrPath: Path + def jobTag: String +} + +final case class WrongReturnCode(jobTag: String, returnCode: Int, stderrPath: Path) extends KnownJobFailureException { + override def getMessage = s"Job $jobTag exited with return code $returnCode which has not been declared as a valid return code. See 'continueOnReturnCode' runtime attribute for more details." +} + +final case class StderrNonEmpty(jobTag: String, stderrLength: Long, stderrPath: Path) extends KnownJobFailureException { + override def getMessage = s"stderr for job $jobTag has length $stderrLength and 'failOnStderr' runtime attribute was true." +} diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala b/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala index a88f07fdb..b6735ffc5 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala @@ -6,7 +6,7 @@ import better.files.File import cromwell.backend.BackendJobExecutionActor.{AbortedResponse, BackendJobExecutionResponse} import cromwell.backend.BackendLifecycleActor.AbortJobCommand import cromwell.backend.async.AsyncBackendJobExecutionActor.{ExecutionMode, JobId, Recover} -import cromwell.backend.async.{AbortedExecutionHandle, AsyncBackendJobExecutionActor, ExecutionHandle, FailedNonRetryableExecutionHandle, PendingExecutionHandle, SuccessfulExecutionHandle} +import cromwell.backend.async.{AbortedExecutionHandle, AsyncBackendJobExecutionActor, ExecutionHandle, FailedNonRetryableExecutionHandle, PendingExecutionHandle, StderrNonEmpty, SuccessfulExecutionHandle, WrongReturnCode} import cromwell.backend.validation._ import cromwell.backend.wdl.Command import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, BackendJobDescriptor, BackendJobLifecycleActor} @@ -375,26 +375,19 @@ trait StandardAsyncExecutionActor extends AsyncBackendJobExecutionActor with Sta lazy val stderrLength: Long = File(jobPaths.stderr).size lazy val returnCode: Try[Int] = Try(File(jobPaths.returnCode).contentAsString).map(_.trim.toInt) - status match { - case _ if failOnStdErr && stderrLength.intValue > 0 => - // returnCode will be None if it couldn't be downloaded/parsed, which will yield a null in the DB - FailedNonRetryableExecutionHandle(new RuntimeException( - s"execution failed: stderr has length $stderrLength"), returnCode.toOption) - case _ if returnCode.isFailure => - val exception = returnCode.failed.get - jobLogger.warn(s"could not download return code file, retrying", exception) - // Return handle to try again. - oldHandle - case _ if returnCode.isFailure => - FailedNonRetryableExecutionHandle(new RuntimeException( - s"execution failed: could not parse return code as integer", returnCode.failed.get)) - case _ if isAbort(returnCode.get) => - AbortedExecutionHandle - case _ if !continueOnReturnCode.continueFor(returnCode.get) => - val message = s"Call ${jobDescriptor.key.tag}: return code was ${returnCode.get}" - FailedNonRetryableExecutionHandle(new RuntimeException(message), returnCode.toOption) - case _ => - handleExecutionSuccess(status, oldHandle, returnCode.get) + if (failOnStdErr && stderrLength.intValue > 0) { + // returnCode will be None if it couldn't be downloaded/parsed, which will yield a null in the DB + FailedNonRetryableExecutionHandle(StderrNonEmpty(jobDescriptor.key.tag, stderrLength, jobPaths.stderr), returnCode.toOption) + } else if (returnCode.isFailure) { + val exception = returnCode.failed.get + FailedNonRetryableExecutionHandle(new RuntimeException( + s"execution failed: could not download or parse return code as integer", exception)) + } else if (isAbort(returnCode.get)) { + AbortedExecutionHandle + } else if (!continueOnReturnCode.continueFor(returnCode.get)) { + FailedNonRetryableExecutionHandle(WrongReturnCode(jobDescriptor.key.tag, returnCode.get, jobPaths.stderr), returnCode.toOption) + } else { + handleExecutionSuccess(status, oldHandle, returnCode.get) } } else { diff --git a/engine/src/main/scala/cromwell/engine/workflow/WorkflowManagerActor.scala b/engine/src/main/scala/cromwell/engine/workflow/WorkflowManagerActor.scala index a19772cd8..766a54f66 100644 --- a/engine/src/main/scala/cromwell/engine/workflow/WorkflowManagerActor.scala +++ b/engine/src/main/scala/cromwell/engine/workflow/WorkflowManagerActor.scala @@ -5,6 +5,7 @@ import akka.actor._ import akka.event.Logging import cats.data.NonEmptyList import com.typesafe.config.{Config, ConfigFactory} +import cromwell.backend.async.KnownJobFailureException import cromwell.core.Dispatcher.EngineDispatcher import cromwell.core.{WorkflowAborted, WorkflowId} import cromwell.engine.backend.BackendSingletonCollection @@ -301,7 +302,11 @@ class WorkflowManagerActor(params: WorkflowManagerActorParams) } private def expandFailureReasons(reasons: Seq[Throwable]) = { - reasons map { reason => + import cromwell.core.path.PathImplicits._ + reasons map { + case reason: KnownJobFailureException => + reason.getMessage + "\n" + s"Check the content of stderr for potential additional information: ${reason.stderrPath.toRealString}" + case reason => reason.getMessage + "\n" + ExceptionUtils.getStackTrace(reason) } mkString "\n" } diff --git a/engine/src/test/scala/cromwell/SimpleWorkflowActorSpec.scala b/engine/src/test/scala/cromwell/SimpleWorkflowActorSpec.scala index 893f56bfa..29e382623 100644 --- a/engine/src/test/scala/cromwell/SimpleWorkflowActorSpec.scala +++ b/engine/src/test/scala/cromwell/SimpleWorkflowActorSpec.scala @@ -119,7 +119,7 @@ class SimpleWorkflowActorSpec extends CromwellTestKitSpec with BeforeAndAfter { } "fail when a call fails" in { - val expectedError = "Call wf_goodbye.goodbye:NA:1: return code was 1" + val expectedError = "Job wf_goodbye.goodbye:NA:1 exited with return code 1 which has not been declared as a valid return code. See 'continueOnReturnCode' runtime attribute for more details." val failureMatcher = FailureMatcher(expectedError) val TestableWorkflowActorAndMetadataPromise(workflowActor, supervisor, promise) = buildWorkflowActor(SampleWdl.GoodbyeWorld, SampleWdl.GoodbyeWorld.wdlJson, workflowId, failureMatcher) val probe = TestProbe() diff --git a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala index f5ada20d9..82575378b 100644 --- a/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala +++ b/supportedBackends/sfs/src/test/scala/cromwell/backend/sfs/SharedFileSystemJobExecutionActorSpec.scala @@ -7,6 +7,7 @@ import better.files._ import com.typesafe.config.ConfigFactory import cromwell.backend.BackendJobExecutionActor.{AbortedResponse, JobFailedNonRetryableResponse, JobSucceededResponse} import cromwell.backend.BackendLifecycleActor.AbortJobCommand +import cromwell.backend.async.WrongReturnCode import cromwell.backend.io.TestWorkflows._ import cromwell.backend.io.{JobPathsWithDocker, TestWorkflows} import cromwell.backend.sfs.TestLocalAsyncJobExecutionActor._ @@ -54,7 +55,7 @@ class SharedFileSystemJobExecutionActorSpec extends TestKitSuite("SharedFileSyst it should "send back an execution failure if the task fails" in { val expectedResponse = - JobFailedNonRetryableResponse(mock[BackendJobDescriptorKey], new RuntimeException(""), Option(1)) + JobFailedNonRetryableResponse(mock[BackendJobDescriptorKey], WrongReturnCode("wf_goodbye.goodbye:NA:1", 1, Paths.get("")), Option(1)) val workflow = TestWorkflow(buildWorkflowDescriptor(GoodbyeWorld), emptyBackendConfig, expectedResponse) val backend = createBackend(jobDescriptorFromSingleCallWorkflow(workflow.workflowDescriptor, Map.empty, WorkflowOptions.empty, runtimeAttributeDefinitions), workflow.config) testWorkflow(workflow, backend) From a751a83f40781b71891efcfc365f5952e9de7e6e Mon Sep 17 00:00:00 2001 From: Miguel Covarrubias Date: Wed, 11 Jan 2017 18:27:28 -0500 Subject: [PATCH 11/84] Fix some issues found during FireCloud Cromwell 24 acceptance testing. --- CHANGELOG.md | 1 + .../jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53bd0ef36..cc0be7d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * `transpose: (Array[Array[X]]) => Array[Array[X]]` compute the matrix transpose for a 2D array. Assumes each inner array has the same length. * By default, `system.abort-jobs-on-terminate` is false when running `java -jar cromwell.jar server`, and true when running `java -jar cromwell.jar run `. * Enable WDL imports when running in Single Workflow Runner Mode. +* Both batch and non-batch REST workflow submissions now require a multipart/form-data encoded body. * Support for sub workflows (see [Annex A](#annex-a---workflow-outputs)) * Enable WDL imports when running in Single Workflow Runner Mode as well as Server Mode * Support for WDL imports through an additional imports.zip parameter diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala index dff6a755c..dc9534546 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala +++ b/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/JesAttributes.scala @@ -38,6 +38,8 @@ object JesAttributes { "maximum-polling-interval", "genomics.compute-service-account", "dockerhub", + "dockerhub.account", + "dockerhub.token", "genomics", "filesystems", "genomics.auth", From 39eb63d1d5e88e379d911737618a85be22ec31a8 Mon Sep 17 00:00:00 2001 From: Thib Date: Thu, 12 Jan 2017 10:58:17 -0500 Subject: [PATCH 12/84] Clean up execution result logic (#1840) * clean up execution success * fix * re fix --- .../backend/async/KnownJobFailureException.scala | 9 +++++ .../standard/StandardAsyncExecutionActor.scala | 45 ++++++++++++++-------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala b/backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala index 4828678b8..e43895e22 100644 --- a/backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala +++ b/backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala @@ -11,6 +11,15 @@ final case class WrongReturnCode(jobTag: String, returnCode: Int, stderrPath: Pa override def getMessage = s"Job $jobTag exited with return code $returnCode which has not been declared as a valid return code. See 'continueOnReturnCode' runtime attribute for more details." } +final case class ReturnCodeIsNotAnInt(jobTag: String, returnCode: String, stderrPath: Path) extends KnownJobFailureException { + override def getMessage = { + if (returnCode.isEmpty) + s"The return code file for job $jobTag was empty." + else + s"Job $jobTag exited with return code $returnCode which couldn't be converted to an Integer." + } +} + final case class StderrNonEmpty(jobTag: String, stderrLength: Long, stderrPath: Path) extends KnownJobFailureException { override def getMessage = s"stderr for job $jobTag has length $stderrLength and 'failOnStderr' runtime attribute was true." } diff --git a/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala b/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala index b6735ffc5..0d97cbbb9 100644 --- a/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala +++ b/backend/src/main/scala/cromwell/backend/standard/StandardAsyncExecutionActor.scala @@ -6,7 +6,7 @@ import better.files.File import cromwell.backend.BackendJobExecutionActor.{AbortedResponse, BackendJobExecutionResponse} import cromwell.backend.BackendLifecycleActor.AbortJobCommand import cromwell.backend.async.AsyncBackendJobExecutionActor.{ExecutionMode, JobId, Recover} -import cromwell.backend.async.{AbortedExecutionHandle, AsyncBackendJobExecutionActor, ExecutionHandle, FailedNonRetryableExecutionHandle, PendingExecutionHandle, StderrNonEmpty, SuccessfulExecutionHandle, WrongReturnCode} +import cromwell.backend.async.{AbortedExecutionHandle, AsyncBackendJobExecutionActor, ExecutionHandle, FailedNonRetryableExecutionHandle, PendingExecutionHandle, ReturnCodeIsNotAnInt, StderrNonEmpty, SuccessfulExecutionHandle, WrongReturnCode} import cromwell.backend.validation._ import cromwell.backend.wdl.Command import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, BackendJobDescriptor, BackendJobLifecycleActor} @@ -373,21 +373,34 @@ trait StandardAsyncExecutionActor extends AsyncBackendJobExecutionActor with Sta try { if (isSuccess(status)) { - lazy val stderrLength: Long = File(jobPaths.stderr).size - lazy val returnCode: Try[Int] = Try(File(jobPaths.returnCode).contentAsString).map(_.trim.toInt) - if (failOnStdErr && stderrLength.intValue > 0) { - // returnCode will be None if it couldn't be downloaded/parsed, which will yield a null in the DB - FailedNonRetryableExecutionHandle(StderrNonEmpty(jobDescriptor.key.tag, stderrLength, jobPaths.stderr), returnCode.toOption) - } else if (returnCode.isFailure) { - val exception = returnCode.failed.get - FailedNonRetryableExecutionHandle(new RuntimeException( - s"execution failed: could not download or parse return code as integer", exception)) - } else if (isAbort(returnCode.get)) { - AbortedExecutionHandle - } else if (!continueOnReturnCode.continueFor(returnCode.get)) { - FailedNonRetryableExecutionHandle(WrongReturnCode(jobDescriptor.key.tag, returnCode.get, jobPaths.stderr), returnCode.toOption) - } else { - handleExecutionSuccess(status, oldHandle, returnCode.get) + lazy val stderrLength: Try[Long] = Try(File(jobPaths.stderr).size) + lazy val returnCodeAsString: Try[String] = Try(File(jobPaths.returnCode).contentAsString) + lazy val returnCodeAsInt: Try[Int] = returnCodeAsString.map(_.trim.toInt) + + (stderrLength, returnCodeAsString, returnCodeAsInt) match { + // Failed to get stderr size -> Retry + case (Failure(exception), _, _) => + jobLogger.warn(s"could not get stderr file size, retrying", exception) + oldHandle + // Failed to get return code content -> Retry + case (_, Failure(exception), _) => + jobLogger.warn(s"could not download return code file, retrying", exception) + oldHandle + // Failed to convert return code content to Int -> Fail + case (_, _, Failure(exception)) => + FailedNonRetryableExecutionHandle(ReturnCodeIsNotAnInt(jobDescriptor.key.tag, returnCodeAsString.get, jobPaths.stderr)) + // Stderr is not empty and failOnStdErr is true -> Fail + case (Success(length), _, _) if failOnStdErr && length.intValue > 0 => + FailedNonRetryableExecutionHandle(StderrNonEmpty(jobDescriptor.key.tag, length, jobPaths.stderr), returnCodeAsInt.toOption) + // Return code is abort code -> Abort + case (_, _, Success(rc)) if isAbort(rc) => + AbortedExecutionHandle + // Return code is not valid -> Fail + case (_, _, Success(rc)) if !continueOnReturnCode.continueFor(rc) => + FailedNonRetryableExecutionHandle(WrongReturnCode(jobDescriptor.key.tag, returnCodeAsInt.get, jobPaths.stderr), returnCodeAsInt.toOption) + // Otherwise -> Succeed + case (_, _, Success(rc)) => + handleExecutionSuccess(status, oldHandle, rc) } } else { From 3042e289e8ee34743452a169f9ac618ae4a9b8e2 Mon Sep 17 00:00:00 2001 From: Khalid Shakir Date: Thu, 12 Jan 2017 15:07:34 -0500 Subject: [PATCH 13/84] Added changelog message regarding webservice timeout --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc0be7d29..e50a45b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * Added ability to override the default zone(s) used by JES via the config structure by setting `genomics.default-zones` in the JES configuration * Added support for new WDL functions: * `length: (Array[X]) => Integer` - report the length of the specified array +* The cromwell server TCP binding timeout is now configurable via the config key `webservice.timeout`, defaulted to the + previous value `5s` (five seconds) via the reference.conf. ## 24 From 5054a036a3d6f6435c7ac11886f7d7aa3f8ea7a8 Mon Sep 17 00:00:00 2001 From: Jeff Gentry Date: Thu, 12 Jan 2017 15:47:04 -0500 Subject: [PATCH 14/84] Move a metadata service config option to the right location (#1853) --- CHANGELOG.md | 1 + core/src/main/resources/reference.conf | 6 ++++-- .../cromwell/services/metadata/impl/MetadataServiceActor.scala | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc0be7d29..4e952e44f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 25 +* Moved the config value `services.MetadataService.metadata-summary-refresh-interval` to `services.MetadataService.config.metadata-summary-refresh-interval` * Cromwell now applies default labels automatically to JES pipeline runs. * Added ability to override the default zone(s) used by JES via the config structure by setting `genomics.default-zones` in the JES configuration * Added support for new WDL functions: diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 765042ea7..b60bac4ec 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -391,8 +391,10 @@ services { } MetadataService { class = "cromwell.services.metadata.impl.MetadataServiceActor" - # Set this value to "Inf" to turn off metadata summary refresh. The default value is currently "2 seconds". - # metadata-summary-refresh-interval = "Inf" + config { + # Set this value to "Inf" to turn off metadata summary refresh. The default value is currently "2 seconds". + # metadata-summary-refresh-interval = "Inf" + } } } diff --git a/services/src/main/scala/cromwell/services/metadata/impl/MetadataServiceActor.scala b/services/src/main/scala/cromwell/services/metadata/impl/MetadataServiceActor.scala index 2ce7c5b92..1f66a5b52 100644 --- a/services/src/main/scala/cromwell/services/metadata/impl/MetadataServiceActor.scala +++ b/services/src/main/scala/cromwell/services/metadata/impl/MetadataServiceActor.scala @@ -17,7 +17,7 @@ import scala.util.{Failure, Success, Try} object MetadataServiceActor { val MetadataSummaryRefreshInterval: Option[FiniteDuration] = { - val duration = Duration(ConfigFactory.load().as[Option[String]]("services.MetadataService.metadata-summary-refresh-interval").getOrElse("2 seconds")) + val duration = Duration(ConfigFactory.load().as[Option[String]]("services.MetadataService.config.metadata-summary-refresh-interval").getOrElse("2 seconds")) if (duration.isFinite()) Option(duration.asInstanceOf[FiniteDuration]) else None } From 9f27c5e925c008a1dc78efa3131ebb8fd0b2eda9 Mon Sep 17 00:00:00 2001 From: Chris Llanwarne Date: Mon, 9 Jan 2017 11:20:41 -0500 Subject: [PATCH 15/84] Custom labels sent to JES --- CHANGELOG.md | 4 ++ README.md | 72 ++++++++++++--------- .../src/main/scala/cromwell/backend/backend.scala | 7 ++- .../test/scala/cromwell/backend/BackendSpec.scala | 4 +- .../core/WorkflowSourceFilesCollection.scala | 30 ++++++--- .../main/scala/cromwell/core/labels/Label.scala | 73 ++++++++++++++++++++++ .../main/scala/cromwell/core}/labels/Labels.scala | 4 +- .../scala/cromwell/core}/labels/LabelSpec.scala | 10 ++- core/src/test/scala/cromwell/util/SampleWdl.scala | 4 +- .../migration/src/main/resources/changelog.xml | 1 + .../changesets/workflow_store_labels_file.xml | 17 +++++ .../slick/tables/WorkflowStoreEntryComponent.scala | 4 +- .../database/sql/tables/WorkflowStoreEntry.scala | 1 + engine/src/main/resources/swagger/cromwell.yaml | 10 +++ .../MaterializeWorkflowDescriptorActor.scala | 39 +++++++++--- .../workflow/workflowstore/SqlWorkflowStore.scala | 16 ++--- .../cromwell/webservice/CromwellApiHandler.scala | 2 +- .../cromwell/webservice/CromwellApiService.scala | 27 +++++--- .../cromwell/webservice/WorkflowJsonSupport.scala | 4 +- .../test/scala/cromwell/CromwellTestKitSpec.scala | 3 +- .../test/scala/cromwell/RestartWorkflowSpec.scala | 3 +- .../scala/cromwell/SimpleWorkflowActorSpec.scala | 2 +- .../MaterializeWorkflowDescriptorActorSpec.scala | 69 ++++++++++++++++---- .../lifecycle/execution/ejea/PerTestHelper.scala | 2 +- .../subworkflowstore/SubWorkflowStoreSpec.scala | 2 +- src/main/scala/cromwell/CromwellCommandLine.scala | 61 +++++++++++------- src/main/scala/cromwell/Main.scala | 10 +-- .../main/scala/cromwell/backend/impl/jes/Run.scala | 27 ++++---- .../cromwell/backend/impl/jes/labels/Label.scala | 54 ---------------- .../jes/JesAsyncBackendJobExecutionActorSpec.scala | 34 ++++++---- .../impl/spark/SparkRuntimeAttributesSpec.scala | 4 +- 31 files changed, 403 insertions(+), 197 deletions(-) create mode 100644 core/src/main/scala/cromwell/core/labels/Label.scala rename {supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes => core/src/main/scala/cromwell/core}/labels/Labels.scala (86%) rename {supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes => core/src/test/scala/cromwell/core}/labels/LabelSpec.scala (84%) create mode 100644 database/migration/src/main/resources/changesets/workflow_store_labels_file.xml delete mode 100644 supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Label.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbb8f2dc..94edff1e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ * `length: (Array[X]) => Integer` - report the length of the specified array * The cromwell server TCP binding timeout is now configurable via the config key `webservice.timeout`, defaulted to the previous value `5s` (five seconds) via the reference.conf. + * `length: (Array[X]) => Integer` - report the length of the specified array. + +### Database schema changes +* Added CUSTOM_LABELS as a field of WORKFLOW_STORE_ENTRY, to store workflow store entries. ## 24 diff --git a/README.md b/README.md index 69c6ccc8d..f73226582 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,6 @@ A [Workflow Management System](https://en.wikipedia.org/wiki/Workflow_management * [Refresh Token](#refresh-token) * [Docker](#docker) * [Monitoring](#monitoring) - * [Labelling Runs](#labelling-runs) - * [Label Escaping and Padding](#label-escaping-and-padding) * [Runtime Attributes](#runtime-attributes) * [Specifying Default Values](#specifying-default-values) * [continueOnReturnCode](#continueonreturncode) @@ -68,6 +66,9 @@ A [Workflow Management System](https://en.wikipedia.org/wiki/Workflow_management * [preemptible](#preemptible) * [Logging](#logging) * [Workflow Options](#workflow-options) +* [Labels](#labels) + * [Custom Labels File](#custom-labels-file) + * [Label Format](#label-format) * [Call Caching](#call-caching) * [Configuring Call Caching](#configuring-call-caching) * [Call Caching Workflow Options](#call-caching-workflow-options) @@ -144,7 +145,7 @@ java -jar cromwell.jar Actions: run [] [] - [] [] + [] [] [] Given a WDL file and JSON file containing the value of the workflow inputs, this will run the workflow locally and @@ -289,6 +290,8 @@ The command to run this WDL, without needing any inputs, workflow options or met $ java -jar cromwell.jar run threestep.wdl - - - /path/to/my_WDLs.zip ``` +The sixth optional parameter is a path to a labels file. See [Labels](#labels) for information and the expected format. + ## server Start a server on port 8000, the API for the server is described in the [REST API](#rest-api) section. @@ -1386,28 +1389,6 @@ In order to monitor metrics (CPU, Memory, Disk usage...) about the VM during Cal The output of this script will be written to a `monitoring.log` file that will be available in the call gcs bucket when the call completes. This feature is meant to run a script in the background during long-running processes. It's possible that if the task is very short that the log file does not flush before de-localization happens and you will end up with a zero byte file. -### Labelling Runs - -Every call to JES from a Cromwell instance will be automatically labelled by Cromwell so that it can be queried about later. The current label set automatically applied is: - -| Key | Value | Example | Notes | -|-----|-------|---------|-------| -| cromwell-workflow-id | The Cromwell ID given to the root workflow (i.e. the ID returned by Cromwell on submission) | cromwell-d4b412c5-bf3d-4169-91b0-1b635ce47a26 | To fit the required [format](#label-escaping-and-padding), we prefix with 'cromwell-' | -| cromwell-workflow-name | The name of the root workflow | my-root-workflow | See [format](#label-escaping-and-padding). | -| cromwell-sub-workflow-name | The name of this job's sub-workflow | my-sub-workflow | Only if the task is called in a subworkflow, otherwise 'n-a'. See also [format](#label-escaping-and-padding). | -| wdl-task-name | The name of the WDL task | my-task | See [format](#label-escaping-and-padding). | -| wdl-call-name | The name of the WDL call of this job | my-call | Different from 'wdl-task-name' if it was called with an alias. See also [format](#label-escaping-and-padding). | - -#### Label Escaping and Padding - -To fit in with the Google schema for labels, label key and value strings must match the regex `[a-z]([-a-z0-9]*[a-z0-9])?` and be between 1 and 63 characters in length. For this reason, Cromwell will modify workflow/task/call names and custom labels as follows: - -- Any capital letters are lowercased. -- Any character which is not one of `[a-z]`, `[0-9]` or `-` will be replaced with `-`. -- If the start character does not match `[a-z]` then prefix with `x--` -- If the final character does not match `[a-z0-9]` then suffix with `--x` -- If the string is too long, only take the first 30 and last 30 characters and add `---` between them. - # Runtime Attributes Runtime attributes are used to customize tasks. Within a task one can specify runtime attributes to customize the environment for the call. @@ -1725,6 +1706,41 @@ Valid keys and their meanings: * **auth_bucket** - (JES backend only) defaults to the the value in **jes_gcs_root**. This should represent a GCS URL that only Cromwell can write to. The Cromwell account is determined by the `google.authScheme` (and the corresponding `google.userAuth` and `google.serviceAuth`) * **monitoring_script** - (JES backend only) Specifies a GCS URL to a script that will be invoked prior to the WDL command being run. For example, if the value for monitoring_script is "gs://bucket/script.sh", it will be invoked as `./script.sh > monitoring.log &`. The value `monitoring.log` file will be automatically de-localized. +# Labels + +Every call in Cromwell is labelled by Cromwell so that it can be queried about later. The current label set automatically applied is: + +| Key | Value | Example | Notes | +|-----|-------|---------|-------| +| cromwell-workflow-id | The Cromwell ID given to the root workflow (i.e. the ID returned by Cromwell on submission) | cromwell-d4b412c5-bf3d-4169-91b0-1b635ce47a26 | To fit the required [format](#label-format), we prefix with 'cromwell-' | +| cromwell-workflow-name | The name of the root workflow | my-root-workflow | | +| cromwell-sub-workflow-name | The name of this job's sub-workflow | my-sub-workflow | Only present if the task is called in a subworkflow. | +| wdl-task-name | The name of the WDL task | my-task | | +| wdl-call-alias | The alias of the WDL call that created this job | my-task-1 | Only present if the task was called with an alias. | + +## Custom Labels File + +Custom labels can also be applied to every call in a workflow by specifying a custom labels file when the workflow is submitted. This file should be in JSON format and contain a set of fields: `"label-key": "label-value" `. For example: +``` +{ + "label-key-1": "label-value-1", + "label-key-2": "label-value-2", + "label-key-3": "label-value-3" +} +``` + +## Label Format + +To fit in with the Google schema for labels, label key and value strings must match the regex `[a-z]([-a-z0-9]*[a-z0-9])?` and be between 1 and 63 characters in length. + +For custom labels, Cromwell will reject any request which is made containing invalid label strings. For automatically applied labels, Cromwell will modify workflow/task/call names to fit the schema, according to the following rules: + +- Any capital letters are lowercased. +- Any character which is not one of `[a-z]`, `[0-9]` or `-` will be replaced with `-`. +- If the start character does not match `[a-z]` then prefix with `x--` +- If the final character does not match `[a-z0-9]` then suffix with `--x` +- If the string is too long, only take the first 30 and last 30 characters and add `---` between them. + # Call Caching Call Caching allows Cromwell to detect when a job has been run in the past so it doesn't have to re-compute results. Cromwell searches the cache of previously run jobs for a one that has the exact same command and exact same inputs. If a previously run job is found in the cache, Cromwell will **copy the results** of the previous job instead of re-running it. @@ -2277,11 +2293,9 @@ This endpoint accepts a POST request with a `multipart/form-data` encoded body. * `wdlSource` - *Required* Contains the WDL file to submit for execution. * `workflowInputs` - *Optional* JSON file containing the inputs. A skeleton file can be generated from [wdltool](https://github.com/broadinstitute/wdltool) using the "inputs" subcommand. -* `workflowInputs_2` - *Optional* JSON file containing the inputs. -* `workflowInputs_3` - *Optional* JSON file containing the inputs. -* `workflowInputs_4` - *Optional* JSON file containing the inputs. -* `workflowInputs_5` - *Optional* JSON file containing the inputs. +* `workflowInputs_n` - *Optional* Where `n` is an integer. JSON file containing the 'n'th set of auxiliary inputs. * `workflowOptions` - *Optional* JSON file containing options for this workflow execution. See the [run](#run) CLI sub-command for some more information about this. +* `customLabels` - *Optional* JSON file containing a set of custom labels to apply to this workflow. See [Labels](#labels) for the expected format. * `wdlDependencies` - *Optional* ZIP file containing WDL files that are used to resolve import statements. Regarding the workflowInputs parameter, in case of key conflicts between multiple input JSON files, higher values of x in workflowInputs_x override lower values. For example, an input specified in workflowInputs_3 will override an input with the same name in workflowInputs or workflowInputs_2. diff --git a/backend/src/main/scala/cromwell/backend/backend.scala b/backend/src/main/scala/cromwell/backend/backend.scala index 82e863ad0..b59b26b5d 100644 --- a/backend/src/main/scala/cromwell/backend/backend.scala +++ b/backend/src/main/scala/cromwell/backend/backend.scala @@ -2,6 +2,7 @@ package cromwell.backend import com.typesafe.config.Config import cromwell.core.WorkflowOptions.WorkflowOption +import cromwell.core.labels.Labels import cromwell.core.{CallKey, WorkflowId, WorkflowOptions} import wdl4s._ import wdl4s.values.WdlValue @@ -34,8 +35,9 @@ object BackendWorkflowDescriptor { def apply(id: WorkflowId, workflow: Workflow, knownValues: Map[FullyQualifiedName, WdlValue], - workflowOptions: WorkflowOptions) = { - new BackendWorkflowDescriptor(id, workflow, knownValues, workflowOptions, List.empty) + workflowOptions: WorkflowOptions, + customLabels: Labels) = { + new BackendWorkflowDescriptor(id, workflow, knownValues, workflowOptions, customLabels, List.empty) } } @@ -46,6 +48,7 @@ case class BackendWorkflowDescriptor(id: WorkflowId, workflow: Workflow, knownValues: Map[FullyQualifiedName, WdlValue], workflowOptions: WorkflowOptions, + customLabels: Labels, breadCrumbs: List[BackendJobBreadCrumb]) { val rootWorkflow = breadCrumbs.headOption.map(_.workflow).getOrElse(workflow) diff --git a/backend/src/test/scala/cromwell/backend/BackendSpec.scala b/backend/src/test/scala/cromwell/backend/BackendSpec.scala index 30bcc4a79..aec7c8ade 100644 --- a/backend/src/test/scala/cromwell/backend/BackendSpec.scala +++ b/backend/src/test/scala/cromwell/backend/BackendSpec.scala @@ -3,6 +3,7 @@ package cromwell.backend import com.typesafe.config.ConfigFactory import cromwell.backend.BackendJobExecutionActor.{BackendJobExecutionResponse, JobFailedNonRetryableResponse, JobFailedRetryableResponse, JobSucceededResponse} import cromwell.backend.io.TestWorkflows._ +import cromwell.core.labels.Labels import cromwell.core.{WorkflowId, WorkflowOptions} import lenthall.exception.AggregatedException import org.scalatest.Matchers @@ -30,7 +31,8 @@ trait BackendSpec extends ScalaFutures with Matchers with Mockito { WorkflowId.randomId(), WdlNamespaceWithWorkflow.load(wdl.replaceAll("RUNTIME", runtime), Seq.empty[ImportResolver]).workflow, inputs, - options + options, + Labels.empty ) } diff --git a/core/src/main/scala/cromwell/core/WorkflowSourceFilesCollection.scala b/core/src/main/scala/cromwell/core/WorkflowSourceFilesCollection.scala index ac7a7d6aa..55fa68b47 100644 --- a/core/src/main/scala/cromwell/core/WorkflowSourceFilesCollection.scala +++ b/core/src/main/scala/cromwell/core/WorkflowSourceFilesCollection.scala @@ -10,14 +10,27 @@ sealed trait WorkflowSourceFilesCollection { def wdlSource: WdlSource def inputsJson: WdlJson def workflowOptionsJson: WorkflowOptionsJson + def labelsJson: WdlJson + + def importsZipFileOption: Option[Array[Byte]] = this match { case _: WorkflowSourceFilesWithoutImports => None - case WorkflowSourceFilesWithDependenciesZip(_, _, _, importsZip) => Option(importsZip) // i.e. Some(importsZip) if our wiring is correct + case WorkflowSourceFilesWithDependenciesZip(_, _, _, _, importsZip) => Option(importsZip) // i.e. Some(importsZip) if our wiring is correct } def copyOptions(workflowOptions: WorkflowOptionsJson) = this match { - case w: WorkflowSourceFilesWithoutImports => WorkflowSourceFilesWithoutImports(w.wdlSource, w.inputsJson, workflowOptions) - case w: WorkflowSourceFilesWithDependenciesZip => WorkflowSourceFilesWithDependenciesZip(w.wdlSource, w.inputsJson, workflowOptions, w.importsZip) + case w: WorkflowSourceFilesWithoutImports => WorkflowSourceFilesWithoutImports( + wdlSource = w.wdlSource, + inputsJson = w.inputsJson, + workflowOptionsJson = workflowOptions, + labelsJson = w.labelsJson) + + case w: WorkflowSourceFilesWithDependenciesZip => WorkflowSourceFilesWithDependenciesZip( + wdlSource = w.wdlSource, + inputsJson = w.inputsJson, + workflowOptionsJson = workflowOptions, + labelsJson = w.labelsJson, + importsZip = w.importsZip) } } @@ -25,19 +38,22 @@ object WorkflowSourceFilesCollection { def apply(wdlSource: WdlSource, inputsJson: WdlJson, workflowOptionsJson: WorkflowOptionsJson, + labelsJson: WdlJson, importsFile: Option[Array[Byte]]): WorkflowSourceFilesCollection = importsFile match { - case Some(imports) => WorkflowSourceFilesWithDependenciesZip(wdlSource, inputsJson, workflowOptionsJson, imports) - case None => WorkflowSourceFilesWithoutImports(wdlSource, inputsJson, workflowOptionsJson) + case Some(imports) => WorkflowSourceFilesWithDependenciesZip(wdlSource, inputsJson, workflowOptionsJson, labelsJson, imports) + case None => WorkflowSourceFilesWithoutImports(wdlSource, inputsJson, workflowOptionsJson, labelsJson) } } final case class WorkflowSourceFilesWithoutImports(wdlSource: WdlSource, inputsJson: WdlJson, - workflowOptionsJson: WorkflowOptionsJson) extends WorkflowSourceFilesCollection + workflowOptionsJson: WorkflowOptionsJson, + labelsJson: WdlJson) extends WorkflowSourceFilesCollection final case class WorkflowSourceFilesWithDependenciesZip(wdlSource: WdlSource, inputsJson: WdlJson, workflowOptionsJson: WorkflowOptionsJson, + labelsJson: WdlJson, importsZip: Array[Byte]) extends WorkflowSourceFilesCollection { - override def toString = s"WorkflowSourceFilesWithDependenciesZip($wdlSource, $inputsJson, $workflowOptionsJson, <>)" + override def toString = s"WorkflowSourceFilesWithDependenciesZip($wdlSource, $inputsJson, $workflowOptionsJson, $labelsJson, <>)" } diff --git a/core/src/main/scala/cromwell/core/labels/Label.scala b/core/src/main/scala/cromwell/core/labels/Label.scala new file mode 100644 index 000000000..4fc1e2959 --- /dev/null +++ b/core/src/main/scala/cromwell/core/labels/Label.scala @@ -0,0 +1,73 @@ +package cromwell.core.labels + +import lenthall.validation.ErrorOr.ErrorOr +import cats.data.Validated._ +import cats.syntax.cartesian._ +import cats.syntax.validated._ + +sealed abstract case class Label(key: String, value: String) + +object Label { + + // Yes, 63. Not a typo for 64. + // See 'labels' in https://cloud.google.com/genomics/reference/rpc/google.genomics.v1alpha2#google.genomics.v1alpha2.RunPipelineArgs + private val MaxLabelLength = 63 + val LabelRegexPattern = "[a-z]([-a-z0-9]*[a-z0-9])?" + + def validateName(s: String): ErrorOr[String] = { + if (LabelRegexPattern.r.pattern.matcher(s).matches) { + if (s.length <= MaxLabelLength) s.validNel else s"Invalid label: $s was ${s.length} characters. The maximum is $MaxLabelLength".invalidNel + } else { + s"Invalid label: $s did not match the regex $LabelRegexPattern".invalidNel + } + } + + def validateLabel(key: String, value: String): ErrorOr[Label] = { + val validatedKey = validateName(key) + val validatedValue = validateName(value) + + (validatedKey |@| validatedValue) map { case (k, v) => new Label(k, v) {} } + } + + /** + * Change to meet the constraint: + * - To match the regex LabelRegexPattern + * - To be between 1 and MaxLabelLength characters total + */ + def safeName(mainText: String): String = { + + validateName(mainText) match { + case Valid(labelText) => labelText + case _ => + def appendSafe(current: String, nextChar: Char): String = { + nextChar match { + case c if c.isLetterOrDigit || c == '-' => current + c.toLower + case _ => current + '-' + } + } + + val foldResult = mainText.toCharArray.foldLeft("")(appendSafe) + + val startsValid = foldResult.headOption.exists(_.isLetter) + val endsValid = foldResult.lastOption.exists(_.isLetterOrDigit) + + val validStart = if (startsValid) foldResult else "x--" + foldResult + val validStartAndEnd = if (endsValid) validStart else validStart + "--x" + + val length = validStartAndEnd.length + val tooLong = length > MaxLabelLength + + if (tooLong) { + val middleSeparator = "---" + val subSectionLength = (MaxLabelLength - middleSeparator.length) / 2 + validStartAndEnd.substring(0, subSectionLength) + middleSeparator + validStartAndEnd.substring(length - subSectionLength, length) + } else { + validStartAndEnd + } + } + } + + def safeLabel(key: String, value: String): Label = { + new Label(safeName(key), safeName(value)) {} + } +} diff --git a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Labels.scala b/core/src/main/scala/cromwell/core/labels/Labels.scala similarity index 86% rename from supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Labels.scala rename to core/src/main/scala/cromwell/core/labels/Labels.scala index 6932e1fc8..8e891fb51 100644 --- a/supportedBackends/jes/src/main/scala/cromwell/backend/impl/jes/labels/Labels.scala +++ b/core/src/main/scala/cromwell/core/labels/Labels.scala @@ -1,4 +1,4 @@ -package cromwell.backend.impl.jes.labels +package cromwell.core.labels import scala.collection.JavaConverters._ @@ -14,4 +14,6 @@ object Labels { val kvps: Seq[(String, String)] = values.toSeq Labels((kvps map { case (k, v) => Label.safeLabel(k, v) }).to[Vector]) } + + def empty = Labels(Vector.empty) } diff --git a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/labels/LabelSpec.scala b/core/src/test/scala/cromwell/core/labels/LabelSpec.scala similarity index 84% rename from supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/labels/LabelSpec.scala rename to core/src/test/scala/cromwell/core/labels/LabelSpec.scala index 41c19140b..47060b6d6 100644 --- a/supportedBackends/jes/src/test/scala/cromwell/backend/impl/jes/labels/LabelSpec.scala +++ b/core/src/test/scala/cromwell/core/labels/LabelSpec.scala @@ -1,5 +1,6 @@ -package cromwell.backend.impl.jes.labels +package cromwell.core.labels +import cats.data.Validated.{Invalid, Valid} import org.scalatest.{FlatSpec, Matchers} class LabelSpec extends FlatSpec with Matchers { @@ -29,13 +30,16 @@ class LabelSpec extends FlatSpec with Matchers { goodLabelStrings foreach { label => it should s"validate the good label string '$label'" in { - Label.validate(label) should be(true) + Label.validateName(label) should be(Valid(label)) } } badLabelConversions foreach { case (label: String, conversion: String) => it should s"not validate the bad label string '$label'" in { - Label.validate(label) should be(false) + Label.validateName(label) match { + case Invalid(_) => // Good! + case Valid(_) => fail(s"Label validation succeeded but should have failed.") + } } it should s"convert the bad label string '$label' into the safe label string '$conversion'" in { diff --git a/core/src/test/scala/cromwell/util/SampleWdl.scala b/core/src/test/scala/cromwell/util/SampleWdl.scala index 2e4d77399..b02b6db9d 100644 --- a/core/src/test/scala/cromwell/util/SampleWdl.scala +++ b/core/src/test/scala/cromwell/util/SampleWdl.scala @@ -14,8 +14,8 @@ import scala.language.postfixOps trait SampleWdl extends TestFileUtil { def wdlSource(runtime: String = ""): WdlSource - def asWorkflowSources(runtime: String = "", workflowOptions: String = "{}") = - WorkflowSourceFilesWithoutImports(wdlSource(runtime), wdlJson, workflowOptions) + def asWorkflowSources(runtime: String = "", workflowOptions: String = "{}", labels: String = "{}") = + WorkflowSourceFilesWithoutImports(wdlSource = wdlSource(runtime), inputsJson = wdlJson, workflowOptionsJson = workflowOptions, labelsJson = labels) val rawInputs: WorkflowRawInputs def name = getClass.getSimpleName.stripSuffix("$") diff --git a/database/migration/src/main/resources/changelog.xml b/database/migration/src/main/resources/changelog.xml index c67137ae0..1deb8d50c 100644 --- a/database/migration/src/main/resources/changelog.xml +++ b/database/migration/src/main/resources/changelog.xml @@ -54,6 +54,7 @@ +