Skip to content

Commit ab48c89

Browse files
authored
Reinstate runtime attributes
1 parent 08e74fa commit ab48c89

File tree

13 files changed

+230
-115
lines changed

13 files changed

+230
-115
lines changed
Lines changed: 91 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
package cromwell.backend
22

3-
import _root_.wdl._
4-
import _root_.wdl.expression.PureStandardLibraryFunctions
53
import akka.actor.{ActorLogging, ActorRef}
64
import akka.event.LoggingReceive
5+
import cats.data.Validated.{Invalid, Valid, _}
6+
import cats.data.{NonEmptyList, ValidatedNel}
7+
import cats.instances.list._
8+
import cats.syntax.traverse._
9+
import common.validation.Validation._
710
import cromwell.backend.BackendLifecycleActor._
811
import cromwell.backend.BackendWorkflowInitializationActor._
9-
import cromwell.backend.async.RuntimeAttributeValidationFailures
12+
import cromwell.backend.async.{RuntimeAttributeValidationFailure, RuntimeAttributeValidationFailures}
1013
import cromwell.backend.validation.ContinueOnReturnCodeValidation
11-
import cromwell.core.{WorkflowMetadataKeys, WorkflowOptions}
14+
import cromwell.core.{NoIoFunctionSet, WorkflowMetadataKeys, WorkflowOptions}
1215
import cromwell.services.metadata.MetadataService.PutMetadataAction
1316
import cromwell.services.metadata.{MetadataEvent, MetadataKey, MetadataValue}
14-
import wom.callable.TaskDefinition
17+
import mouse.all._
18+
import wom.expression.WomExpression
1519
import wom.graph.TaskCallNode
1620
import wom.types.WomType
1721
import wom.values.WomValue
1822

1923
import scala.concurrent.Future
20-
import scala.util.{Failure, Success, Try}
24+
import scala.util.Try
2125

2226
object BackendWorkflowInitializationActor {
2327

@@ -32,6 +36,66 @@ object BackendWorkflowInitializationActor {
3236
case class InitializationSuccess(backendInitializationData: Option[BackendInitializationData]) extends InitializationResponse
3337
case class InitializationFailed(reason: Throwable) extends Exception with InitializationResponse
3438

39+
/**
40+
* This method calls into `runtimeAttributeValidators` to validate runtime attribute expressions.
41+
* The initialization-time validators try to fully `evaluate` (not `evaluateType`) the runtime attribute expression.
42+
* Because these validators do full evaluations but at initialization time we don't have the actual call inputs, it's
43+
* possible a legitimate runtime attribute expression evaluation could fail to evaluate. These validators must
44+
* therefore give runtime attribute expressions which failed to evaluate the benefit of the doubt and pass them.
45+
*
46+
* Perhaps a better way of doing initialization-time runtime attribute validation:
47+
*
48+
* - Currently `default_runtime_attributes` are values and not expressions. These should all be typechecked
49+
* irrespective of whether there is actual fallback to using their values due to missing task attributes.
50+
*
51+
* - It appears it should be possible to fully typecheck runtime attribute expressions at workflow initialization
52+
* time if task (not call!) declarations and inputs are considered. We have all tasks at workflow initialization
53+
* time, and the task declarations know their types. We won't actually have a call until all of its inputs are
54+
* available, but tasks are available in the `WdlNamespace`. A task-aware type evaluation might look like:
55+
*
56+
* {{{
57+
* val task: Task = ???
58+
* val expression: WdlExpression = ???
59+
* def lookup(name: String): WomType = task.declarations.find(_.name == name).get.womType
60+
* val expressionType = expression.evaluateType(lookup, OnlyPureFunctions)
61+
* }}}
62+
*
63+
* - Assuming the previous two checks pass, an attempt should be made to evaluate the expression. It's possible
64+
* the expression could be a function of call inputs or declarations which are unavailable at initialization time,
65+
* so these failed evaluations must continue to be accepted as possibly correct expressions.
66+
*
67+
* - Assuming the expression evaluation succeeds, it should be possible to do real business logic validation.
68+
*
69+
* - It would be nice to memoize as much of the work that gets done here as possible so it doesn't have to all be
70+
* repeated when the various `FooRuntimeAttributes` classes are created, in the spirit of #1076.
71+
*/
72+
def validateRuntimeAttributes(
73+
taskName: String,
74+
defaultRuntimeAttributes: Map[String, WomValue],
75+
runtimeAttributes: Map[String, WomExpression],
76+
runtimeAttributeValidators: Map[String, Option[WomExpression] => Boolean]
77+
): ValidatedNel[RuntimeAttributeValidationFailure, Unit] = {
78+
79+
//This map append will overwrite default key/values with runtime settings upon key collisions
80+
val lookups = defaultRuntimeAttributes.mapValues(_.asWomExpression) ++ runtimeAttributes
81+
82+
runtimeAttributeValidators.toList.traverse[WishKindProjectorWorked, Unit]{
83+
case (attributeName, validator) =>
84+
val runtimeAttributeValue: Option[WomExpression] = lookups.get(attributeName)
85+
validator(runtimeAttributeValue).fold(
86+
validNel(()),
87+
Invalid(NonEmptyList.of(RuntimeAttributeValidationFailure(taskName, attributeName, runtimeAttributeValue)))
88+
)
89+
}.map(_ => ())
90+
}
91+
92+
//This exists because when a type (in this case Validated[A,B]) has 2 type parameters it can't figure
93+
//out how to form a shape F[X], where X could be either A or B. In this case we want an Applicative
94+
//for ValidatedNel where the error type is bound to RuntimeAttributeValidationFailure.
95+
//Ideally we could use Scala Compiler plugin "Kind projector" to do this inline but Scaladoc
96+
//did not work with it :(.
97+
private type WishKindProjectorWorked[A] = ValidatedNel[RuntimeAttributeValidationFailure, A]
98+
3599
}
36100

37101
/**
@@ -53,15 +117,14 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
53117
* declarations will fail evaluation and return `true` from this predicate, even if the type could be determined
54118
* to be wrong with consideration of task declarations or inputs.
55119
*/
56-
protected def wdlTypePredicate(valueRequired: Boolean, predicate: WomType => Boolean)(wdlExpressionMaybe: Option[WomValue]): Boolean = {
57-
wdlExpressionMaybe match {
120+
protected def womTypePredicate(valueRequired: Boolean, predicate: WomType => Boolean)(womExpressionMaybe: Option[WomExpression]): Boolean = {
121+
womExpressionMaybe match {
58122
case None => !valueRequired
59-
case Some(wdlExpression: WdlExpression) =>
60-
wdlExpression.evaluate(NoLookup, PureStandardLibraryFunctions) map (_.womType) match {
61-
case Success(womType) => predicate(womType)
62-
case Failure(_) => true // If we can't evaluate it, we'll let it pass for now...
123+
case Some(womExpression: WomExpression) =>
124+
womExpression.evaluateValue(Map.empty, NoIoFunctionSet) map (_.womType) match {
125+
case Valid(womType) => predicate(womType)
126+
case Invalid(_) => true // If we can't evaluate it, we'll let it pass for now...
63127
}
64-
case Some(womValue) => predicate(womValue.womType)
65128
}
66129
}
67130

@@ -70,11 +133,11 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
70133
* between evaluation failures due to missing call inputs or evaluation failures due to malformed expressions, and will
71134
* return `true` in both cases.
72135
*/
73-
protected def continueOnReturnCodePredicate(valueRequired: Boolean)(wdlExpressionMaybe: Option[WomValue]): Boolean = {
74-
ContinueOnReturnCodeValidation.default(configurationDescriptor.backendRuntimeConfig).validateOptionalExpression(wdlExpressionMaybe)
136+
protected def continueOnReturnCodePredicate(valueRequired: Boolean)(womExpressionMaybe: Option[WomValue]): Boolean = {
137+
ContinueOnReturnCodeValidation.default(configurationDescriptor.backendRuntimeConfig).validateOptionalWomValue(womExpressionMaybe)
75138
}
76139

77-
protected def runtimeAttributeValidators: Map[String, Option[WomValue] => Boolean]
140+
protected def runtimeAttributeValidators: Map[String, Option[WomExpression] => Boolean]
78141

79142
// FIXME: If a workflow executes jobs using multiple backends,
80143
// each backend will try to write its own workflow root and override any previous one.
@@ -85,66 +148,6 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
85148

86149
protected def coerceDefaultRuntimeAttributes(options: WorkflowOptions): Try[Map[String, WomValue]]
87150

88-
/**
89-
* This method calls into `runtimeAttributeValidators` to validate runtime attribute expressions.
90-
* The initialization-time validators try to fully `evaluate` (not `evaluateType`) the runtime attribute expression.
91-
* Because these validators do full evaluations but at initialization time we don't have the actual call inputs, it's
92-
* possible a legitimate runtime attribute expression evaluation could fail to evaluate. These validators must
93-
* therefore give runtime attribute expressions which failed to evaluate the benefit of the doubt and pass them.
94-
*
95-
* Perhaps a better way of doing initialization-time runtime attribute validation:
96-
*
97-
* - Currently `default_runtime_attributes` are values and not expressions. These should all be typechecked
98-
* irrespective of whether there is actual fallback to using their values due to missing task attributes.
99-
*
100-
* - It appears it should be possible to fully typecheck runtime attribute expressions at workflow initialization
101-
* time if task (not call!) declarations and inputs are considered. We have all tasks at workflow initialization
102-
* time, and the task declarations know their types. We won't actually have a call until all of its inputs are
103-
* available, but tasks are available in the `WdlNamespace`. A task-aware type evaluation might look like:
104-
*
105-
* {{{
106-
* val task: Task = ???
107-
* val expression: WdlExpression = ???
108-
* def lookup(name: String): WomType = task.declarations.find(_.name == name).get.womType
109-
* val expressionType = expression.evaluateType(lookup, OnlyPureFunctions)
110-
* }}}
111-
*
112-
* - Assuming the previous two checks pass, an attempt should be made to evaluate the expression. It's possible
113-
* the expression could be a function of call inputs or declarations which are unavailable at initialization time,
114-
* so these failed evaluations must continue to be accepted as possibly correct expressions.
115-
*
116-
* - Assuming the expression evaluation succeeds, it should be possible to do real business logic validation.
117-
*
118-
* - It would be nice to memoize as much of the work that gets done here as possible so it doesn't have to all be
119-
* repeated when the various `FooRuntimeAttributes` classes are created, in the spirit of #1076.
120-
*/
121-
private def validateRuntimeAttributes: Future[Unit] = {
122-
123-
coerceDefaultRuntimeAttributes(workflowDescriptor.workflowOptions) match {
124-
case Success(_) =>
125-
126-
// def defaultRuntimeAttribute(name: String): Option[WomValue] = {
127-
// defaultRuntimeAttributes.get(name)
128-
// }
129-
130-
def badRuntimeAttrsForTask(task: TaskDefinition) = {
131-
// TODO WOM: https://github.com/broadinstitute/cromwell/issues/2606
132-
// runtimeAttributeValidators map { case (attributeName, validator) =>
133-
// val value = task.runtimeAttributes.attributes.get(attributeName) orElse defaultRuntimeAttribute(attributeName)
134-
// attributeName -> ((value, validator(value)))
135-
// } collect {
136-
// case (name, (value, false)) => RuntimeAttributeValidationFailure(task.name, name, value)
137-
// }
138-
Seq.empty
139-
}
140-
141-
calls map { _.callable } flatMap badRuntimeAttrsForTask match {
142-
case errors if errors.isEmpty => Future.successful(())
143-
case errors => Future.failed(RuntimeAttributeValidationFailures(errors.toList))
144-
}
145-
case Failure(t) => Future.failed(t)
146-
}
147-
}
148151

149152
def receive: Receive = LoggingReceive {
150153
case Initialize => performActionThenRespond(initSequence(), onFailure = InitializationFailed)
@@ -154,11 +157,17 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
154157
/**
155158
* Our predefined sequence to run during preStart
156159
*/
157-
final def initSequence(): Future[InitializationSuccess] = for {
158-
_ <- validateRuntimeAttributes
159-
_ <- validate()
160-
data <- beforeAll()
161-
} yield InitializationSuccess(data)
160+
final def initSequence(): Future[InitializationSuccess] =
161+
for {
162+
defaultRuntimeAttributes <- coerceDefaultRuntimeAttributes(workflowDescriptor.workflowOptions) |> Future.fromTry _
163+
taskList = calls.toList.map(_.callable).map(t => t.name -> t.runtimeAttributes.attributes)
164+
_ <- taskList.
165+
traverse[WishKindProjectorWorked, Unit]{
166+
case (name, runtimeAttributes) => validateRuntimeAttributes(name, defaultRuntimeAttributes, runtimeAttributes, runtimeAttributeValidators)
167+
}.toFuture(errors => RuntimeAttributeValidationFailures(errors.toList))
168+
_ <- validate()
169+
data <- beforeAll()
170+
} yield InitializationSuccess(data)
162171

163172
/**
164173
* Abort all initializations.
@@ -176,3 +185,4 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
176185
def validate(): Future[Unit]
177186

178187
}
188+

backend/src/main/scala/cromwell/backend/async/KnownJobFailureException.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package cromwell.backend.async
22

33
import cromwell.core.path.Path
44
import common.exception.ThrowableAggregation
5-
import wom.values.WomValue
5+
import cromwell.core.NoIoFunctionSet
6+
import wom.expression.WomExpression
67

78
abstract class KnownJobFailureException extends Exception {
89
def stderrPath: Option[Path]
@@ -29,14 +30,14 @@ final case class StderrNonEmpty(jobTag: String, stderrLength: Long, stderrPath:
2930
object RuntimeAttributeValidationFailure {
3031
def apply(jobTag: String,
3132
runtimeAttributeName: String,
32-
runtimeAttributeValue: Option[WomValue]): RuntimeAttributeValidationFailure = RuntimeAttributeValidationFailure(jobTag, runtimeAttributeName, runtimeAttributeValue, None)
33+
runtimeAttributeValue: Option[WomExpression]): RuntimeAttributeValidationFailure = RuntimeAttributeValidationFailure(jobTag, runtimeAttributeName, runtimeAttributeValue, None)
3334
}
3435

3536
final case class RuntimeAttributeValidationFailure private (jobTag: String,
3637
runtimeAttributeName: String,
37-
runtimeAttributeValue: Option[WomValue],
38+
runtimeAttributeValue: Option[WomExpression],
3839
stderrPath: Option[Path]) extends KnownJobFailureException {
39-
override def getMessage = s"Task $jobTag has an invalid runtime attribute $runtimeAttributeName = ${runtimeAttributeValue map { _.valueString} getOrElse "!! NOT FOUND !!"}"
40+
override def getMessage = s"Task $jobTag has an invalid runtime attribute $runtimeAttributeName = ${runtimeAttributeValue map { _.evaluateValue(Map.empty, NoIoFunctionSet)} getOrElse "!! NOT FOUND !!"}"
4041
}
4142

4243
final case class RuntimeAttributeValidationFailures(throwables: List[RuntimeAttributeValidationFailure]) extends KnownJobFailureException with ThrowableAggregation {

backend/src/main/scala/cromwell/backend/standard/StandardInitializationActor.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import cromwell.backend.wfs.WorkflowPathBuilder
77
import cromwell.backend.{BackendConfigurationDescriptor, BackendInitializationData, BackendWorkflowDescriptor, BackendWorkflowInitializationActor}
88
import cromwell.core.WorkflowOptions
99
import cromwell.core.path.{DefaultPathBuilder, PathBuilder}
10+
import wom.expression.WomExpression
1011
import wom.graph.TaskCallNode
1112
import wom.values.WomValue
1213

@@ -71,7 +72,7 @@ class StandardInitializationActor(val standardParams: StandardInitializationActo
7172
def runtimeAttributesBuilder: StandardValidatedRuntimeAttributesBuilder =
7273
StandardValidatedRuntimeAttributesBuilder.default(configurationDescriptor.backendRuntimeConfig)
7374

74-
override protected lazy val runtimeAttributeValidators: Map[String, (Option[WomValue]) => Boolean] = {
75+
override protected lazy val runtimeAttributeValidators: Map[String, (Option[WomExpression]) => Boolean] = {
7576
runtimeAttributesBuilder.validatorMap
7677
}
7778

backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package cromwell.backend.validation
22

3+
import cats.data.Validated.{Invalid, Valid}
34
import cats.data.{NonEmptyList, Validated}
45
import cats.syntax.validated._
56
import com.typesafe.config.Config
67
import cromwell.backend.{MemorySize, RuntimeAttributeDefinition}
78
import common.validation.ErrorOr._
9+
import cromwell.core.NoIoFunctionSet
810
import org.slf4j.Logger
911
import wdl.expression.PureStandardLibraryFunctions
1012
import wdl.{NoLookup, WdlExpression}
13+
import wom.expression.WomExpression
1114
import wom.types._
1215
import wom.values._
1316

@@ -312,7 +315,7 @@ trait RuntimeAttributesValidation[ValidatedType] {
312315
/**
313316
* Runs this validation on the value matching key.
314317
*
315-
* NOTE: The values passed to this method should already be evaluated instances of WomValue, and not WdlExpression.
318+
* NOTE: The values passed to this method should already be evaluated instances of WomValue, and not WomExpression.
316319
*
317320
* @param values The full set of values.
318321
* @return The error or valid value for this key.
@@ -341,7 +344,7 @@ trait RuntimeAttributesValidation[ValidatedType] {
341344
* @param wdlExpressionMaybe The optional expression.
342345
* @return True if the expression may be evaluated.
343346
*/
344-
def validateOptionalExpression(wdlExpressionMaybe: Option[WomValue]): Boolean = {
347+
def validateOptionalWomValue(wdlExpressionMaybe: Option[WomValue]): Boolean = {
345348
wdlExpressionMaybe match {
346349
case None => staticDefaultOption.isDefined || validateNone.isValid
347350
case Some(wdlExpression: WdlExpression) =>
@@ -353,6 +356,16 @@ trait RuntimeAttributesValidation[ValidatedType] {
353356
}
354357
}
355358

359+
def validateOptionalWomExpression(womExpressionMaybe: Option[WomExpression]): Boolean = {
360+
womExpressionMaybe match {
361+
case None => staticDefaultOption.isDefined || validateNone.isValid
362+
case Some(womExpression) =>
363+
womExpression.evaluateValue(Map.empty, NoIoFunctionSet) match {
364+
case Valid(womValue) => validateExpression.applyOrElse(womValue, (_: Any) => false)
365+
case Invalid(_) => true // If we can't evaluate it, we'll let it pass for now...
366+
}
367+
}
368+
}
356369
/**
357370
* Used to convert this instance to a `RuntimeAttributeDefinition`.
358371
*

backend/src/main/scala/cromwell/backend/validation/ValidatedRuntimeAttributesBuilder.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import cromwell.backend.RuntimeAttributeDefinition
66
import common.exception.MessageAggregation
77
import common.validation.ErrorOr._
88
import org.slf4j.Logger
9+
import wom.expression.WomExpression
910
import wom.types.WomType
1011
import wom.values.WomValue
1112

@@ -35,9 +36,9 @@ trait ValidatedRuntimeAttributesBuilder {
3536
/**
3637
* Returns validators suitable for BackendWorkflowInitializationActor.runtimeAttributeValidators.
3738
*/
38-
final lazy val validatorMap: Map[String, (Option[WomValue]) => Boolean] = {
39+
final lazy val validatorMap: Map[String, (Option[WomExpression]) => Boolean] = {
3940
validations.map(validation =>
40-
validation.key -> validation.validateOptionalExpression _
41+
validation.key -> validation.validateOptionalWomExpression _
4142
).toMap
4243
}
4344

0 commit comments

Comments
 (0)