Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WX-1468] Implement returnCodes runtime attribute #7389

Merged
merged 28 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ jobs:
set -e
echo Running test.sh
./src/ci/bin/test.sh
# If a build step fails, activate SSH and idle for 30 minutes.
- name: Setup tmate session
if: ${{ failure() }}
uses: mxschmitt/action-tmate@v3
timeout-minutes: 90
with:
limit-access-to-actor: true
detached: true
# always() is some github magic that forces the following step to run, even when the previous fails.
# Without it, the if statement won't be evaluated on a test failure.
- uses: ravsamhq/notify-slack-action@v2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import cromwell.backend.BackendLifecycleActor._
import cromwell.backend.BackendWorkflowInitializationActor._
import cromwell.backend.async.{RuntimeAttributeValidationFailure, RuntimeAttributeValidationFailures}
import cromwell.backend.validation.ContinueOnReturnCodeValidation
import cromwell.backend.validation.{ContinueOnReturnCodeValidation, ReturnCodesValidation}
import cromwell.core._
import cromwell.services.metadata.{MetadataEvent, MetadataKey, MetadataValue}
import cromwell.services.metadata.MetadataService.PutMetadataAction
Expand Down Expand Up @@ -134,6 +134,16 @@
.default(configurationDescriptor.backendRuntimeAttributesConfig)
.validateOptionalWomValue(womExpressionMaybe)

/**
* This predicate is only appropriate for validation during workflow initialization. The logic does not differentiate
* between evaluation failures due to missing call inputs or evaluation failures due to malformed expressions, and will
* return `true` in both cases.
*/
protected def returnCodesPredicate(valueRequired: Boolean)(womExpressionMaybe: Option[WomValue]): Boolean =
ReturnCodesValidation
.default(configurationDescriptor.backendRuntimeAttributesConfig)
.validateOptionalWomValue(womExpressionMaybe)

Check warning on line 145 in backend/src/main/scala/cromwell/backend/BackendWorkflowInitializationActor.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/BackendWorkflowInitializationActor.scala#L145

Added line #L145 was not covered by tests

protected def runtimeAttributeValidators: Map[String, Option[WomExpression] => Boolean]

// FIXME: If a workflow executes jobs using multiple backends,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -752,9 +752,26 @@ trait StandardAsyncExecutionActor
*
* @return the behavior for continuing on the return code.
*/
lazy val continueOnReturnCode: ContinueOnReturnCode =
lazy val continueOnReturnCode: ReturnCode =
RuntimeAttributesValidation.extract(ContinueOnReturnCodeValidation.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.
*/
lazy val returnCodes: ReturnCode =
RuntimeAttributesValidation.extract(ReturnCodesValidation.instance, validatedRuntimeAttributes)

/**
* Returns the true behavior for continuing on the return code. If `returnCodes` runtime attribute is specified,
* then `continueOnReturnCode` attribute is ignored. If `returnCodes` is unspecified, then the value in
* `continueOnReturnCode` will be used.
*/
lazy val returnCode: ReturnCode =
if (returnCodes.isInstanceOf[ReturnCodeSet] && returnCodes.equals(ReturnCodeSet(Set(0)))) continueOnReturnCode
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is checking whether we have the default value for returnCodes and falling back to continueOnReturnCode if we do. What if someone included the default value in their WDL? Can we insert the default after we choose which attribute to use?

If not, we could build the default check into ReturnCode, so we can call something like returnCodes.isDefault?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just for fun, a more idiomatic Scala way to write this is:

returnCodes match {
  case ReturnCodeSet(Set(0)) => continueOnReturnCode
  case _ => returnCodes
}

else returnCodes
rsaperst marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns the max number of times that a failed job should be retried, obtained by converting `maxRetries` to an Int.
*/
Expand Down Expand Up @@ -1388,7 +1405,7 @@ trait StandardAsyncExecutionActor
)
)
retryElseFail(executionHandle)
case Success(returnCodeAsInt) if continueOnReturnCode.continueFor(returnCodeAsInt) =>
case Success(returnCodeAsInt) if returnCode.continueFor(returnCodeAsInt) =>
handleExecutionSuccess(status, oldHandle, returnCodeAsInt)
// It's important that we check retryWithMoreMemory case before isAbort. RC could be 137 in either case;
// if it was caused by OOM killer, want to handle as OOM and not job abort.
Expand Down Expand Up @@ -1422,7 +1439,7 @@ trait StandardAsyncExecutionActor
} else {
tryReturnCodeAsInt match {
case Success(returnCodeAsInt)
if outOfMemoryDetected && memoryRetryRequested && !continueOnReturnCode.continueFor(returnCodeAsInt) =>
if outOfMemoryDetected && memoryRetryRequested && !returnCode.continueFor(returnCodeAsInt) =>
rsaperst marked this conversation as resolved.
Show resolved Hide resolved
val executionHandle = Future.successful(
FailedNonRetryableExecutionHandle(
RetryWithMoreMemory(jobDescriptor.key.tag, stderrAsOption, memoryRetryErrorKeys, log),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ object StandardValidatedRuntimeAttributesBuilder {
def default(backendRuntimeConfig: Option[Config]): StandardValidatedRuntimeAttributesBuilder = {
val required = Seq(
ContinueOnReturnCodeValidation.default(backendRuntimeConfig),
ReturnCodesValidation.default(backendRuntimeConfig),
FailOnStderrValidation.default(backendRuntimeConfig),
MaxRetriesValidation.default(backendRuntimeConfig)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@
/**
* Decides if a call/job continues upon a specific return code.
*/
sealed trait ContinueOnReturnCode {
sealed trait ContinueOnReturnCode extends ReturnCode {

/**
* Returns true if the call is a success based on the return code.
*
* @param returnCode Return code from the process / script.
* @return True if the call is a success.
*/
final def continueFor(returnCode: Int): Boolean =
final override def continueFor(returnCode: Int): Boolean =
this match {
case ContinueOnReturnCodeFlag(continue) => continue || returnCode == 0
case ContinueOnReturnCodeSet(returnCodes) => returnCodes.contains(returnCode)
case _ => super.continueFor(returnCode)

Check warning on line 23 in backend/src/main/scala/cromwell/backend/validation/ContinueOnReturnCode.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ContinueOnReturnCode.scala#L23

Added line #L23 was not covered by tests
}
}

Expand All @@ -31,14 +31,3 @@
case class ContinueOnReturnCodeFlag(continue: Boolean) extends ContinueOnReturnCode {
override def toString = continue.toString
}

/**
* Continues only if the call/job return code is found in returnCodes.
* @param returnCodes Inclusive set of return codes that specify a job success.
*/
case class ContinueOnReturnCodeSet(returnCodes: Set[Int]) extends ContinueOnReturnCode {
override def toString = returnCodes match {
case single if single.size == 1 => returnCodes.head.toString
case multiple => s"[${multiple.mkString(",")}]"
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package cromwell.backend.validation

import cats.data.Validated.{Invalid, Valid}
import cats.implicits._
import com.typesafe.config.Config
import cromwell.backend.validation.RuntimeAttributesValidation._
import common.validation.ErrorOr._
import wom.RuntimeAttributesKeys
import wom.types._
Expand All @@ -23,42 +21,29 @@ import scala.util.Try
* `default` a validation with the default value specified by the reference.conf file.
*/
object ContinueOnReturnCodeValidation {
lazy val instance: RuntimeAttributesValidation[ContinueOnReturnCode] = new ContinueOnReturnCodeValidation
def default(runtimeConfig: Option[Config]): RuntimeAttributesValidation[ContinueOnReturnCode] =
lazy val instance: RuntimeAttributesValidation[ReturnCode] = new ContinueOnReturnCodeValidation
def default(runtimeConfig: Option[Config]): RuntimeAttributesValidation[ReturnCode] =
instance.withDefault(configDefaultWdlValue(runtimeConfig) getOrElse WomInteger(0))
def configDefaultWdlValue(runtimeConfig: Option[Config]): Option[WomValue] =
instance.configDefaultWomValue(runtimeConfig)
}

class ContinueOnReturnCodeValidation extends RuntimeAttributesValidation[ContinueOnReturnCode] {
class ContinueOnReturnCodeValidation extends ReturnCodeValidation {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to see if we can collapse all the return code stuff into this validation, and have this be the last thing in the code path that's aware of the two different attributes. We could even continue calling it ContinueOnReturnCode to minimize the diff. This validation can look at both keys and make a decision about what ContinueOnReturnCode object to build. We can turn the * into a ContinueOnReturnCodeFlag(true), which seems to have the same meaning.


override def key: String = RuntimeAttributesKeys.ContinueOnReturnCodeKey

override def coercion: Set[WomType] = ContinueOnReturnCode.validWdlTypes

override def validateValue: PartialFunction[WomValue, ErrorOr[ContinueOnReturnCode]] = {
override def validateValue: PartialFunction[WomValue, ErrorOr[ReturnCode]] = {
case WomBoolean(value) => ContinueOnReturnCodeFlag(value).validNel
case WomString(value) if Try(value.toBoolean).isSuccess => ContinueOnReturnCodeFlag(value.toBoolean).validNel
case WomString(value) if Try(value.toInt).isSuccess => ContinueOnReturnCodeSet(Set(value.toInt)).validNel
case WomInteger(value) => ContinueOnReturnCodeSet(Set(value)).validNel
case value @ WomArray(_, seq) =>
val errorOrInts: ErrorOr[List[Int]] = (seq.toList map validateInt).sequence[ErrorOr, Int]
errorOrInts match {
case Valid(ints) => ContinueOnReturnCodeSet(ints.toSet).validNel
case Invalid(_) => invalidValueFailure(value)
}
case value => super.validateValue(value)
}

override def validateExpression: PartialFunction[WomValue, Boolean] = {
case WomBoolean(_) => true
case WomString(value) if Try(value.toInt).isSuccess => true
case WomString(value) if Try(value.toBoolean).isSuccess => true
case WomInteger(_) => true
case WomArray(WomArrayType(WomStringType), elements) =>
elements forall { value =>
Try(value.valueString.toInt).isSuccess
}
case WomArray(WomArrayType(WomIntegerType), _) => true
case value => super.validateExpression(value)
}

override protected def missingValueMessage: String = s"Expecting $key" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cromwell.backend.validation

trait ReturnCode {

/**
* Returns true if the call is a success based on the return code.
*
* @param returnCode Return code from the process / script.
* @return True if the call is a success.
*/
def continueFor(returnCode: Int): Boolean =
this match {
case ReturnCodeSet(returnCodes) => returnCodes.contains(returnCode)
}
}

/**
* Continues only if the call/job return code is found in returnCodes.
* @param returnCodes Inclusive set of return codes that specify a job success.
*/
case class ReturnCodeSet(returnCodes: Set[Int]) extends ReturnCode {
override def toString = returnCodes match {
case single if single.size == 1 => returnCodes.head.toString
case multiple => s"[${multiple.mkString(",")}]"
}

/**
* Returns true if the call is a success based on the return code.
*
* @param returnCode Return code from the process / script.
* @return True if the call is a success.
*/
override def continueFor(returnCode: Int): Boolean = super.continueFor(returnCode)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method looks like a no-op, since it overrides the super method and then calls it.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cromwell.backend.validation

import cats.data.Validated.{Invalid, Valid}
import cats.implicits.{catsSyntaxValidatedId, toTraverseOps}
import common.validation.ErrorOr.ErrorOr
import cromwell.backend.validation.RuntimeAttributesValidation.validateInt
import wom.types.{WomArrayType, WomIntegerType, WomStringType, WomType}
import wom.values.{WomArray, WomInteger, WomString, WomValue}

import scala.util.Try

/**
* Validates the `continueOnReturnCode` and `returnCodes` runtime attributes, since they share the functionality of
* being a Set of Integers.
*/
trait ReturnCodeValidation extends RuntimeAttributesValidation[ReturnCode] {
override def key: String
override def coercion: Iterable[WomType]
override def validateValue: PartialFunction[WomValue, ErrorOr[ReturnCode]] = {
case WomString(value) if Try(value.toInt).isSuccess => ReturnCodeSet(Set(value.toInt)).validNel

Check warning on line 20 in backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala#L20

Added line #L20 was not covered by tests
case WomInteger(value) => ReturnCodeSet(Set(value)).validNel
case value @ WomArray(_, seq) =>
val errorOrInts: ErrorOr[List[Int]] = (seq.toList map validateInt).sequence[ErrorOr, Int]
errorOrInts match {
case Valid(ints) => ReturnCodeSet(ints.toSet).validNel
case Invalid(_) => invalidValueFailure(value)

Check warning on line 26 in backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala#L26

Added line #L26 was not covered by tests
}
case value => invalidValueFailure(value)

Check warning on line 28 in backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala#L28

Added line #L28 was not covered by tests
}

override def validateExpression: PartialFunction[WomValue, Boolean] = {
case WomString(value) if Try(value.toInt).isSuccess => true
case WomInteger(_) => true
case WomArray(WomArrayType(WomStringType), elements) =>
elements forall { value =>
Try(value.valueString.toInt).isSuccess

Check warning on line 36 in backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala#L35-L36

Added lines #L35 - L36 were not covered by tests
}
case WomArray(WomArrayType(WomIntegerType), _) => true
case _ => false
}

override def usedInCallCaching: Boolean = true

Check warning on line 42 in backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodeValidation.scala#L42

Added line #L42 was not covered by tests
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cromwell.backend.validation

import wom.types._

object ReturnCodes {
val validWdlTypes = Set[WomType](WomArrayType(WomIntegerType), WomStringType, WomIntegerType)
}

/**
* Decides if a call/job continues upon a specific return code.
*/
sealed trait ReturnCodes extends ReturnCode {

/**
* Returns true if the call is a success based on the return code.
*
* @param returnCode Return code from the process / script.
* @return True if the call is a success.
*/
final override def continueFor(returnCode: Int): Boolean =
this match {
case ReturnCodesString(continue) => continue.equals("*") || returnCode == 0
case _ => super.continueFor(returnCode)

Check warning on line 23 in backend/src/main/scala/cromwell/backend/validation/ReturnCodes.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodes.scala#L23

Added line #L23 was not covered by tests
}
}

/**
* Continues based on a string, if "*" all return codes continue.
* @param returnCode If "*", all return codes are valid for continuing.
*/
case class ReturnCodesString(returnCode: String) extends ReturnCodes {
override def toString = returnCode
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cromwell.backend.validation

import cats.implicits.catsSyntaxValidatedId
import com.typesafe.config.Config
import common.validation.ErrorOr.ErrorOr
import wom.RuntimeAttributesKeys
import wom.types.WomType
import wom.values.{WomInteger, WomString, WomValue}

/**
* Validates the "returnCodes" runtime attribute as an Integer, returning the value as an `Int`, or as
* an array of Integers, returning the value as Array[Int], or "*", returning the value as a String.
*
* `default` a hardcoded default WomValue for returnCodes.
*
* `configDefaultWdlValue` returns the value of the attribute as specified by the
* reference.conf file, coerced into a WomValue.
*/
object ReturnCodesValidation {
lazy val instance: RuntimeAttributesValidation[ReturnCode] = new ReturnCodesValidation
lazy val optional: OptionalRuntimeAttributesValidation[ReturnCode] = instance.optional
def default(runtimeConfig: Option[Config]): RuntimeAttributesValidation[ReturnCode] =
instance.withDefault(configDefaultWdlValue(runtimeConfig) getOrElse WomInteger(0))

def configDefaultWdlValue(runtimeConfig: Option[Config]): Option[WomValue] =
instance.configDefaultWomValue(runtimeConfig)
def configDefaultWomValue(config: Option[Config]): Option[WomValue] = instance.configDefaultWomValue(config)

Check warning on line 27 in backend/src/main/scala/cromwell/backend/validation/ReturnCodesValidation.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodesValidation.scala#L27

Added line #L27 was not covered by tests
}

class ReturnCodesValidation extends ReturnCodeValidation {

override def key: String = RuntimeAttributesKeys.ReturnCodesKey

override def coercion: Set[WomType] = ReturnCodes.validWdlTypes

override def validateValue: PartialFunction[WomValue, ErrorOr[ReturnCode]] = {
case WomString(value) if value.equals("*") =>
ReturnCodesString(value).validNel
case value => super.validateValue(value)
}

override def validateExpression: PartialFunction[WomValue, Boolean] = {
case WomString(value) if value.equals("*") => true
case value => super.validateExpression(value)
}

override protected def missingValueMessage: String = s"Expecting $key" +

Check warning on line 47 in backend/src/main/scala/cromwell/backend/validation/ReturnCodesValidation.scala

View check run for this annotation

Codecov / codecov/patch

backend/src/main/scala/cromwell/backend/validation/ReturnCodesValidation.scala#L47

Added line #L47 was not covered by tests
" runtime attribute to be either a String '*' or an Array[Int]"

override def usedInCallCaching: Boolean = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@

def validateContinueOnReturnCode(value: Option[WomValue],
onMissingKey: => ErrorOr[ContinueOnReturnCode]
): ErrorOr[ContinueOnReturnCode] =
): ErrorOr[ReturnCode] =
validateWithValidation(value, ContinueOnReturnCodeValidation.instance, onMissingKey)

def validateReturnCodes(value: Option[WomValue], onMissingKey: => ErrorOr[ReturnCode]): ErrorOr[ReturnCode] =
validateWithValidation(value, ReturnCodesValidation.instance, onMissingKey)

Check warning on line 41 in backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala

View check run for this annotation

Codecov / codecov/patch

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

Added line #L41 was not covered by tests

def validateMemory(value: Option[WomValue], onMissingKey: => ErrorOr[MemorySize]): ErrorOr[MemorySize] =
validateWithValidation(value, MemoryValidation.instance(), onMissingKey)

Expand Down Expand Up @@ -372,7 +375,8 @@
case Success(womValue) => validateExpression.applyOrElse(womValue, (_: Any) => false)
case Failure(_) => true // If we can't evaluate it, we'll let it pass for now...
}
case Some(womValue) => validateExpression.applyOrElse(womValue, (_: Any) => false)
case Some(womValue) =>
validateExpression.applyOrElse(womValue, (_: Any) => false)

Check warning on line 379 in backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala

View check run for this annotation

Codecov / codecov/patch

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

Added line #L379 was not covered by tests
}

def validateOptionalWomExpression(womExpressionMaybe: Option[WomExpression]): Boolean =
Expand Down
Loading
Loading