Yicong-Huang commented on code in PR #6143:
URL: https://github.com/apache/texera/pull/6143#discussion_r3606735822
##########
common/workflow-compiler/src/main/scala/org/apache/texera/common/compiler/WorkflowCompiler.scala:
##########
@@ -116,111 +118,159 @@ object WorkflowCompiler {
}
case class WorkflowCompilationResult(
+ logicalPlan: LogicalPlan,
physicalPlan: Option[PhysicalPlan], // if physical plan is none, the
compilation is failed
operatorIdToOutputSchemas: Map[OperatorIdentity, Map[PortIdentity,
Option[Schema]]],
- operatorIdToError: Map[OperatorIdentity, WorkflowFatalError]
+ operatorIdToError: Map[OperatorIdentity, WorkflowFatalError],
+ outputPortsNeedingStorage: Set[GlobalPortIdentity]
)
class WorkflowCompiler(
context: WorkflowContext
) extends LazyLogging {
- // function to expand logical plan to physical plan
+ /**
+ * Function to expand logical plan to physical plan
+ * @return the expanded physical plan and a set of output ports that need
storage
+ */
private def expandLogicalPlan(
logicalPlan: LogicalPlan,
+ logicalOpsToViewResult: List[String],
errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]]
- ): PhysicalPlan = {
+ ): (PhysicalPlan, Set[GlobalPortIdentity]) = {
+ val terminalLogicalOps = logicalPlan.getTerminalOperatorIds
+ val logicalOpsNeedingStorage =
+ (terminalLogicalOps ++
logicalOpsToViewResult.map(OperatorIdentity(_))).toSet
var physicalPlan = PhysicalPlan(operators = Set.empty, links = Set.empty)
+ val outputPortsNeedingStorage: mutable.HashSet[GlobalPortIdentity] =
mutable.HashSet()
- logicalPlan.getTopologicalOpIds.asScala.foreach { logicalOpId =>
- val logicalOp = logicalPlan.getOperator(logicalOpId)
- val allUpstreamLinks =
logicalPlan.getUpstreamLinks(logicalOp.operatorIdentifier)
+ logicalPlan.getTopologicalOpIds.asScala.foreach(logicalOpId =>
+ Try {
+ val logicalOp = logicalPlan.getOperator(logicalOpId)
- try {
val subPlan = logicalOp.getPhysicalPlan(context.workflowId,
context.executionId)
-
subPlan
.topologicalIterator()
.map(subPlan.getOperator)
- .foreach { physicalOp =>
- val externalLinks = allUpstreamLinks
- .filter(link => physicalOp.inputPorts.contains(link.toPortId))
- .flatMap { link =>
- physicalPlan
- .getPhysicalOpsOfLogicalOp(link.fromOpId)
- .find(_.outputPorts.contains(link.fromPortId))
- .map(fromOp =>
- PhysicalLink(fromOp.id, link.fromPortId, physicalOp.id,
link.toPortId)
- )
- }
+ .foreach({ physicalOp =>
+ {
+ val externalLinks = logicalPlan
+ .getUpstreamLinks(logicalOp.operatorIdentifier)
+ .filter(link => physicalOp.inputPorts.contains(link.toPortId))
+ .flatMap { link =>
+ physicalPlan
+ .getPhysicalOpsOfLogicalOp(link.fromOpId)
+ .find(_.outputPorts.contains(link.fromPortId))
+ .map(fromOp =>
+ PhysicalLink(fromOp.id, link.fromPortId, physicalOp.id,
link.toPortId)
+ )
+ }
- val internalLinks = subPlan.getUpstreamPhysicalLinks(physicalOp.id)
+ val internalLinks =
subPlan.getUpstreamPhysicalLinks(physicalOp.id)
- // Add the operator to the physical plan
- physicalPlan =
physicalPlan.addOperator(physicalOp.propagateSchema())
+ // Add the operator to the physical plan
+ physicalPlan =
physicalPlan.addOperator(physicalOp.propagateSchema())
- // Add all the links to the physical plan
- physicalPlan = (externalLinks ++
internalLinks).foldLeft(physicalPlan) { (plan, link) =>
- plan.addLink(link)
- }
+ // Add all the links to the physical plan
+ physicalPlan = (externalLinks ++ internalLinks)
+ .foldLeft(physicalPlan) { (plan, link) => plan.addLink(link) }
- // **Check for Python-based operator errors during code
generation**
- if (physicalOp.isPythonBased) {
- val code = physicalOp.getCode
- val exceptionPattern = """#EXCEPTION DURING CODE
GENERATION:\s*(.*)""".r
+ // **Check for Python-based operator errors during code
generation**
+ if (physicalOp.isPythonBased) {
+ val code = physicalOp.getCode
+ val exceptionPattern = """#EXCEPTION DURING CODE
GENERATION:\s*(.*)""".r
- exceptionPattern.findFirstMatchIn(code).foreach { matchResult =>
- val errorMessage = matchResult.group(1).trim
- val error =
- new RuntimeException(s"Operator is not configured properly:
$errorMessage")
+ exceptionPattern.findFirstMatchIn(code).foreach { matchResult
=>
+ val errorMessage = matchResult.group(1).trim
+ val error =
+ new RuntimeException(s"Operator is not configured
properly: $errorMessage")
- errorList match {
- case Some(list) => list.append((logicalOpId, error)) //
Store error and continue
- case None => throw error // Throw immediately if no
error list is provided
+ errorList match {
+ case Some(list) => list.append((logicalOpId, error)) //
Store error and continue
+ case None => throw error // Throw immediately if no
error list is provided
+ }
}
}
}
+ })
+
+ // convert logical operators needing storage to output ports needing
storage
+ subPlan
+ .topologicalIterator()
+ .filter(opId => logicalOpsNeedingStorage.contains(opId.logicalOpId))
+ .map(physicalPlan.getOperator)
+ .foreach { physicalOp =>
+ physicalOp.outputPorts
+ .filterNot(_._1.internal)
+ .foreach {
+ case (outputPortId, _) =>
+ outputPortsNeedingStorage += GlobalPortIdentity(
+ opId = physicalOp.id,
+ portId = outputPortId
+ )
+ }
}
- } catch {
- case e: Throwable =>
+ } match {
+ case Success(_) =>
+
+ case Failure(err) =>
errorList match {
- case Some(list) => list.append((logicalOpId, e)) // Store error
- case None => throw e // Throw if no list is provided
+ case Some(list) => list.append((logicalOpId, err))
+ case None => throw err
}
}
- }
-
- physicalPlan
+ )
+ (physicalPlan, outputPortsNeedingStorage.toSet)
}
/**
- * Compile a workflow to physical plan, along with the schema propagation
result and error(if any)
+ * Compile a workflow to physical plan, along with the schema propagation
result and error(if any).
*
* @param logicalPlanPojo the pojo parsed from workflow str provided by user
- * @return WorkflowCompilationResult, containing the physical plan, input
schemas per op and error per op
+ * @param errorHandling Lenient (editing-time, collect all errors) or
Strict (pre-execution, throw)
+ * @return WorkflowCompilationResult, containing the logical plan, physical
plan, output schemas per
+ * op, errors per op, and the output ports that need storage
*/
def compile(
- logicalPlanPojo: LogicalPlanPojo
+ logicalPlanPojo: LogicalPlanPojo,
+ errorHandling: CompilationErrorHandling =
CompilationErrorHandling.Lenient
): WorkflowCompilationResult = {
- val errorList = new ArrayBuffer[(OperatorIdentity, Throwable)]()
- var opIdToOutputSchema: Map[OperatorIdentity, Map[PortIdentity,
Option[Schema]]] = Map()
+ // Lenient collects into a buffer; Strict passes None so the first error
is thrown.
+ val errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]] =
+ errorHandling match {
+ case CompilationErrorHandling.Lenient =>
+ Some(new ArrayBuffer[(OperatorIdentity, Throwable)]())
+ case CompilationErrorHandling.Strict => None
+ }
+
// 1. convert the pojo to logical plan
val logicalPlan: LogicalPlan = LogicalPlan(logicalPlanPojo)
// 2. resolve the file name in each scan source operator
- logicalPlan.resolveScanSourceOpFileName(Some(errorList))
+ logicalPlan.resolveScanSourceOpFileName(errorList)
- // 3. expand the logical plan to the physical plan
- val physicalPlan = expandLogicalPlan(logicalPlan, Some(errorList))
+ // 3. expand the logical plan to the physical plan, and get the output
ports that need storage
+ val (physicalPlan, outputPortsNeedingStorage) =
+ expandLogicalPlan(logicalPlan, logicalPlanPojo.opsToViewResult,
errorList)
// 4. collect the output schema for each logical op
- // even if error is encountered when logical => physical, we still want to
get the input schemas for rest no-error operators
- opIdToOutputSchema = collectOutputSchemaFromPhysicalPlan(physicalPlan,
errorList)
+ // even if error is encountered when logical => physical, we still want to
get the input schemas
+ // for the rest of the no-error operators. In Strict mode there is no
buffer (errors already thrown),
+ // so schema errors go to a throwaway buffer and do not affect the result.
+ val schemaErrorList = errorList.getOrElse(new
ArrayBuffer[(OperatorIdentity, Throwable)]())
Review Comment:
Good catch. Went with the wording fix in
bbbf9f5841677f4708c4c17c3b77f98889fcae80: the comment now states explicitly
that schema-propagation failures are NOT thrown in Strict and surface as `None`
schemas (legacy execution-path behavior), and a new spec pins exactly that
(`currently not throw on schema-propagation errors`). Making Strict throw there
is a behavior change I'd rather do as a follow-up.
##########
common/workflow-compiler/src/main/scala/org/apache/texera/common/compiler/WorkflowCompiler.scala:
##########
@@ -116,111 +118,159 @@ object WorkflowCompiler {
}
case class WorkflowCompilationResult(
+ logicalPlan: LogicalPlan,
physicalPlan: Option[PhysicalPlan], // if physical plan is none, the
compilation is failed
operatorIdToOutputSchemas: Map[OperatorIdentity, Map[PortIdentity,
Option[Schema]]],
- operatorIdToError: Map[OperatorIdentity, WorkflowFatalError]
+ operatorIdToError: Map[OperatorIdentity, WorkflowFatalError],
+ outputPortsNeedingStorage: Set[GlobalPortIdentity]
)
class WorkflowCompiler(
context: WorkflowContext
) extends LazyLogging {
- // function to expand logical plan to physical plan
+ /**
+ * Function to expand logical plan to physical plan
+ * @return the expanded physical plan and a set of output ports that need
storage
+ */
private def expandLogicalPlan(
logicalPlan: LogicalPlan,
+ logicalOpsToViewResult: List[String],
errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]]
- ): PhysicalPlan = {
+ ): (PhysicalPlan, Set[GlobalPortIdentity]) = {
+ val terminalLogicalOps = logicalPlan.getTerminalOperatorIds
+ val logicalOpsNeedingStorage =
+ (terminalLogicalOps ++
logicalOpsToViewResult.map(OperatorIdentity(_))).toSet
var physicalPlan = PhysicalPlan(operators = Set.empty, links = Set.empty)
+ val outputPortsNeedingStorage: mutable.HashSet[GlobalPortIdentity] =
mutable.HashSet()
- logicalPlan.getTopologicalOpIds.asScala.foreach { logicalOpId =>
- val logicalOp = logicalPlan.getOperator(logicalOpId)
- val allUpstreamLinks =
logicalPlan.getUpstreamLinks(logicalOp.operatorIdentifier)
+ logicalPlan.getTopologicalOpIds.asScala.foreach(logicalOpId =>
+ Try {
Review Comment:
Right — `Try` catches only non-fatal errors, so a fatal error during
expansion now fails the `/compile` request instead of being recorded
per-operator. Keeping it: swallowing fatal JVM errors into a per-operator error
map was arguably the worse behavior. Called this out in the updated PR
description as a known minor behavior change.
##########
common/workflow-compiler/src/main/scala/org/apache/texera/common/compiler/CompilationErrorHandling.scala:
##########
@@ -17,25 +17,18 @@
* under the License.
*/
-package org.apache.texera.amber.compiler.model
+package org.apache.texera.common.compiler
-import com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty}
-import org.apache.texera.amber.core.virtualidentity.OperatorIdentity
-import org.apache.texera.amber.core.workflow.PortIdentity
+/**
+ * Controls how the compiler reacts to a per-operator compilation error.
+ *
+ * - [[CompilationErrorHandling.Lenient]] collects every error and returns
them in the result,
+ * with `physicalPlan = None` when any error occurred. Used for
editing-time validation.
+ * - [[CompilationErrorHandling.Strict]] throws on the first error. Used
before execution.
Review Comment:
Fixed in bbbf9f5841677f4708c4c17c3b77f98889fcae80 — the scaladoc now says
Strict throws during file resolution and logical-to-physical expansion, but
schema-propagation failures surface as `None` schemas (follow-up tracks failing
fast there too).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]