1
1
package cromwell .backend
2
2
3
- import _root_ .wdl ._
4
- import _root_ .wdl .expression .PureStandardLibraryFunctions
5
3
import akka .actor .{ActorLogging , ActorRef }
6
4
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 ._
7
10
import cromwell .backend .BackendLifecycleActor ._
8
11
import cromwell .backend .BackendWorkflowInitializationActor ._
9
- import cromwell .backend .async .RuntimeAttributeValidationFailures
12
+ import cromwell .backend .async .{ RuntimeAttributeValidationFailure , RuntimeAttributeValidationFailures }
10
13
import cromwell .backend .validation .ContinueOnReturnCodeValidation
11
- import cromwell .core .{WorkflowMetadataKeys , WorkflowOptions }
14
+ import cromwell .core .{NoIoFunctionSet , WorkflowMetadataKeys , WorkflowOptions }
12
15
import cromwell .services .metadata .MetadataService .PutMetadataAction
13
16
import cromwell .services .metadata .{MetadataEvent , MetadataKey , MetadataValue }
14
- import wom .callable .TaskDefinition
17
+ import mouse .all ._
18
+ import wom .expression .WomExpression
15
19
import wom .graph .TaskCallNode
16
20
import wom .types .WomType
17
21
import wom .values .WomValue
18
22
19
23
import scala .concurrent .Future
20
- import scala .util .{ Failure , Success , Try }
24
+ import scala .util .Try
21
25
22
26
object BackendWorkflowInitializationActor {
23
27
@@ -32,6 +36,66 @@ object BackendWorkflowInitializationActor {
32
36
case class InitializationSuccess (backendInitializationData : Option [BackendInitializationData ]) extends InitializationResponse
33
37
case class InitializationFailed (reason : Throwable ) extends Exception with InitializationResponse
34
38
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
+
35
99
}
36
100
37
101
/**
@@ -53,15 +117,14 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
53
117
* declarations will fail evaluation and return `true` from this predicate, even if the type could be determined
54
118
* to be wrong with consideration of task declarations or inputs.
55
119
*/
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 {
58
122
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...
63
127
}
64
- case Some (womValue) => predicate(womValue.womType)
65
128
}
66
129
}
67
130
@@ -70,11 +133,11 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
70
133
* between evaluation failures due to missing call inputs or evaluation failures due to malformed expressions, and will
71
134
* return `true` in both cases.
72
135
*/
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 )
75
138
}
76
139
77
- protected def runtimeAttributeValidators : Map [String , Option [WomValue ] => Boolean ]
140
+ protected def runtimeAttributeValidators : Map [String , Option [WomExpression ] => Boolean ]
78
141
79
142
// FIXME: If a workflow executes jobs using multiple backends,
80
143
// each backend will try to write its own workflow root and override any previous one.
@@ -85,66 +148,6 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
85
148
86
149
protected def coerceDefaultRuntimeAttributes (options : WorkflowOptions ): Try [Map [String , WomValue ]]
87
150
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
- }
148
151
149
152
def receive : Receive = LoggingReceive {
150
153
case Initialize => performActionThenRespond(initSequence(), onFailure = InitializationFailed )
@@ -154,11 +157,17 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
154
157
/**
155
158
* Our predefined sequence to run during preStart
156
159
*/
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)
162
171
163
172
/**
164
173
* Abort all initializations.
@@ -176,3 +185,4 @@ trait BackendWorkflowInitializationActor extends BackendWorkflowLifecycleActor w
176
185
def validate (): Future [Unit ]
177
186
178
187
}
188
+
0 commit comments