Skip to content

[SPARK-48845][SQL] GenericUDF catch exceptions from children #47268

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

Conversation

jackylee-ch
Copy link
Contributor

@jackylee-ch jackylee-ch commented Jul 9, 2024

What changes were proposed in this pull request?

This pr is trying to fix the syntax issues with GenericUDF since 3.5.0. The problem arose from DeferredObject currently passing a value instead of a function, which prevented users from catching exceptions in GenericUDF, resulting in semantic differences.

Here is an example case we encountered. Originally, the semantics were that udf_exception would throw an exception, while udf_catch_exception could catch the exception and return a null value. However, currently, any exception encountered by udf_exception will cause the program to fail.

select udf_catch_exception(udf_exception(col1)) from table

Why are the changes needed?

For before Spark 3.5, we directly made the GenericUDF's DeferredObject lazy and evaluated the children in function.evaluate(deferredObjects).
Now, we would run the children's code first. If an exception is thrown, we would make it lazy to GenericUDF's DeferredObject.

Does this PR introduce any user-facing change?

No.

How was this patch tested?

Newly added UT.

Was this patch authored or co-authored using generative AI tooling?

No.

@github-actions github-actions bot added the SQL label Jul 9, 2024
@jackylee-ch jackylee-ch force-pushed the generic_udf_catch_exception_from_child_func branch from f097982 to eb2b0a2 Compare July 9, 2024 12:52
@LuciferYang
Copy link
Contributor

cc @cloud-fan @yaooqinn @panbingkun FYI

@cloud-fan
Copy link
Contributor

Can we explain the execution path before Spark 3.5? Is this PR trying to follow the previous execution path?

@jackylee-ch
Copy link
Contributor Author

Can we explain the execution path before Spark 3.5? Is this PR trying to follow the previous execution path?

For Spark 3.3, the HiveGenericUDF would set lazy functions to deferredObjects and evaluate in function.evaluate, which is same with the non-codegen mode in this pr.

@yaooqinn
Copy link
Member

Is it possible to combine the code generation and non-code generation code?

@jackylee-ch
Copy link
Contributor Author

Is it possible to combine the code generation and non-code generation code?

AFAIK, it's hard to set lazy function to DeferredObject. I have tried with lambda functions and inner classes to put eval.code in it. The Janino does not support lambda. Additionally, the eval.code relies on other variables, such as columnartorow_batchIdx_0 from BoundReference, which cannot be accessed in an inner class.

Any better ideas?

@@ -131,6 +131,13 @@ class HiveGenericUDFEvaluator(
def setArg(index: Int, arg: Any): Unit =
deferredObjects(index).asInstanceOf[DeferredObjectAdapter].set(arg)

def setFuncArg(index: Int, arg: () => Any): Unit =
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that setFuncArg and setException can be merged?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a little ugly that we use inner class in codegen. If we use setFuncArg, the codegen for exceptions would be follow code.

        s"""
           |try {
           |  ${eval.code}
           |  if (${eval.isNull}) {
           |    $refEvaluator.setArg($i, null);
           |  } else {
           |    $refEvaluator.setArg($i, ${eval.value});
           |  }
           |} catch (Exception exp) {
           |  final exception = exp;
           |  $refEvaluator.setFundArg($i, new scala.Function0<Object>() {
           |    public Object apply() {
           |      throw exception;
           |    }
           |  });
           |}
           |""".stripMargin

| $refEvaluator.setArg($i, null);
| } else {
| $refEvaluator.setArg($i, ${eval.value});
| }
Copy link
Contributor

@panbingkun panbingkun Jul 10, 2024

Choose a reason for hiding this comment

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

  • It seems that catch (...) is only related to ${eval.code}
  • Why is it Exception instead of Throwable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems that catch (...) is only related to ${eval.code}

Yes, we should catch the exceptions from child's executions.

Why is it Exception instead of Throwable?

The Exception can catch most of throwable problems. It's ok to be change to Throwable.

@panbingkun
Copy link
Contributor

panbingkun commented Jul 10, 2024

Will it cover multiple nested scenes?
The solution here seems to still bypass the child's lazy execution, right?

@jackylee-ch
Copy link
Contributor Author

Will it cover multiple nested scenes? The solution here seems to still bypass the child's lazy execution, right?

Yes, it cover the multiple nested scenes. And yes the children.evaluate execute first, but we postpone the handling of Exceptions to ensure that the children's exceptions can be obtained during GenericUDF.eval. When facing multiple layers of GenericUDF, if the innermost child throws an exception, we also gradually execute the child GenericUDFs and handle the thrown exceptions layer by layer until the top layer.

Copy link
Member

@yaooqinn yaooqinn left a comment

Choose a reason for hiding this comment

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

LGTM from myside

|} else {
| $refEvaluator.setArg($i, ${eval.value});
|try {
| ${eval.code}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is slightly different from the interpreted path. In codegen we still eagerly execute the function inputs, but delay the throw of errors. I understand that it's hard to be truly lazy in codegen, can we update the interpreted path instead to make it the same as codegen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure and done. I have updated the code for non-codegen mode and the description for this pr.

test("SPARK-48845: GenericUDF catch exceptions from child UDFs") {
withTable("test_catch_exception") {
Seq("9", "9-1").toDF("a").write.saveAsTable("test_catch_exception")
sql("create temporary function udf_exception as " +
Copy link
Contributor

Choose a reason for hiding this comment

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

let's wrap with withUserDefinedFunction to clean up the functions at the end.

@panbingkun
Copy link
Contributor

Regarding the failure of GA, I guess you need to rebase master. 😄

Copy link
Contributor

@LuciferYang LuciferYang left a comment

Choose a reason for hiding this comment

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

LGTM

@LuciferYang
Copy link
Contributor

@panbingkun any other comments?

@panbingkun
Copy link
Contributor

any other comments?

No, go ahead.

@LuciferYang
Copy link
Contributor

[error] /home/runner/work/spark/spark/sql/hive/src/test/java/org/apache/spark/sql/hive/execution/UDFThrowException.java:24:1:  error: incompatible types: int cannot be converted to String
[error]     return Integer.parseInt(data);
[error] 

@jackylee-ch

@yaooqinn
Copy link
Member

Oh my bad @LuciferYang

@yaooqinn yaooqinn closed this in 236d957 Jul 12, 2024
yaooqinn added a commit that referenced this pull request Jul 12, 2024
### What changes were proposed in this pull request?
This pr is trying to fix the syntax issues with GenericUDF since 3.5.0. The problem arose from DeferredObject currently passing a value instead of a function, which prevented users from catching exceptions in GenericUDF, resulting in semantic differences.

Here is an example case we encountered. Originally, the semantics were that udf_exception would throw an exception, while udf_catch_exception could catch the exception and return a null value. However, currently, any exception encountered by udf_exception will cause the program to fail.
```
select udf_catch_exception(udf_exception(col1)) from table
```

### Why are the changes needed?
For before Spark 3.5, we directly made the GenericUDF's DeferredObject lazy and evaluated the children in `function.evaluate(deferredObjects)`.
Now, we would run the children's code first. If an exception is thrown, we would make it lazy to GenericUDF's DeferredObject.

### Does this PR introduce _any_ user-facing change?
No.

### How was this patch tested?
Newly added UT.

### Was this patch authored or co-authored using generative AI tooling?
No.

Closes #47268 from jackylee-ch/generic_udf_catch_exception_from_child_func.

Lead-authored-by: jackylee-ch <lijunqing@baidu.com>
Co-authored-by: Kent Yao <yao@apache.org>
Signed-off-by: Kent Yao <yao@apache.org>
(cherry picked from commit 236d957)
Signed-off-by: Kent Yao <yao@apache.org>
@yaooqinn
Copy link
Member

Thank you all.

Merged to master and branch-3.5 for the coming 3.5.2

@jackylee-ch
Copy link
Contributor Author

Thanks for your review. @LuciferYang @yaooqinn @cloud-fan @panbingkun

@jackylee-ch jackylee-ch deleted the generic_udf_catch_exception_from_child_func branch July 12, 2024 11:00
jingz-db pushed a commit to jingz-db/spark that referenced this pull request Jul 22, 2024
### What changes were proposed in this pull request?
This pr is trying to fix the syntax issues with GenericUDF since 3.5.0. The problem arose from DeferredObject currently passing a value instead of a function, which prevented users from catching exceptions in GenericUDF, resulting in semantic differences.

Here is an example case we encountered. Originally, the semantics were that udf_exception would throw an exception, while udf_catch_exception could catch the exception and return a null value. However, currently, any exception encountered by udf_exception will cause the program to fail.
```
select udf_catch_exception(udf_exception(col1)) from table
```

### Why are the changes needed?
For before Spark 3.5, we directly made the GenericUDF's DeferredObject lazy and evaluated the children in `function.evaluate(deferredObjects)`.
Now, we would run the children's code first. If an exception is thrown, we would make it lazy to GenericUDF's DeferredObject.

### Does this PR introduce _any_ user-facing change?
No.

### How was this patch tested?
Newly added UT.

### Was this patch authored or co-authored using generative AI tooling?
No.

Closes apache#47268 from jackylee-ch/generic_udf_catch_exception_from_child_func.

Lead-authored-by: jackylee-ch <lijunqing@baidu.com>
Co-authored-by: Kent Yao <yao@apache.org>
Signed-off-by: Kent Yao <yao@apache.org>
attilapiros pushed a commit to attilapiros/spark that referenced this pull request Oct 4, 2024
### What changes were proposed in this pull request?
This pr is trying to fix the syntax issues with GenericUDF since 3.5.0. The problem arose from DeferredObject currently passing a value instead of a function, which prevented users from catching exceptions in GenericUDF, resulting in semantic differences.

Here is an example case we encountered. Originally, the semantics were that udf_exception would throw an exception, while udf_catch_exception could catch the exception and return a null value. However, currently, any exception encountered by udf_exception will cause the program to fail.
```
select udf_catch_exception(udf_exception(col1)) from table
```

### Why are the changes needed?
For before Spark 3.5, we directly made the GenericUDF's DeferredObject lazy and evaluated the children in `function.evaluate(deferredObjects)`.
Now, we would run the children's code first. If an exception is thrown, we would make it lazy to GenericUDF's DeferredObject.

### Does this PR introduce _any_ user-facing change?
No.

### How was this patch tested?
Newly added UT.

### Was this patch authored or co-authored using generative AI tooling?
No.

Closes apache#47268 from jackylee-ch/generic_udf_catch_exception_from_child_func.

Lead-authored-by: jackylee-ch <lijunqing@baidu.com>
Co-authored-by: Kent Yao <yao@apache.org>
Signed-off-by: Kent Yao <yao@apache.org>
himadripal pushed a commit to himadripal/spark that referenced this pull request Oct 19, 2024
### What changes were proposed in this pull request?
This pr is trying to fix the syntax issues with GenericUDF since 3.5.0. The problem arose from DeferredObject currently passing a value instead of a function, which prevented users from catching exceptions in GenericUDF, resulting in semantic differences.

Here is an example case we encountered. Originally, the semantics were that udf_exception would throw an exception, while udf_catch_exception could catch the exception and return a null value. However, currently, any exception encountered by udf_exception will cause the program to fail.
```
select udf_catch_exception(udf_exception(col1)) from table
```

### Why are the changes needed?
For before Spark 3.5, we directly made the GenericUDF's DeferredObject lazy and evaluated the children in `function.evaluate(deferredObjects)`.
Now, we would run the children's code first. If an exception is thrown, we would make it lazy to GenericUDF's DeferredObject.

### Does this PR introduce _any_ user-facing change?
No.

### How was this patch tested?
Newly added UT.

### Was this patch authored or co-authored using generative AI tooling?
No.

Closes apache#47268 from jackylee-ch/generic_udf_catch_exception_from_child_func.

Lead-authored-by: jackylee-ch <lijunqing@baidu.com>
Co-authored-by: Kent Yao <yao@apache.org>
Signed-off-by: Kent Yao <yao@apache.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants