cloud-fan commented on code in PR #59697:
URL: https://github.com/apache/spark/pull/59697#discussion_r4346067541
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/catalog/SessionCatalog.scala:
##########
@@ -2167,12 +2203,49 @@ class SessionCatalog(
if (funcDefinition.isUserDefinedFunction) {
UserDefinedFunction.fromCatalogFunction(funcDefinition,
parser).toExpressionInfo
} else {
- makeExprInfoForHiveFunction(funcDefinition)
+ makeExprInfoForClassFunction(funcDefinition)
}
}
registry.registerFunction(identToRegister, resolvedInfo, functionBuilder)
}
+ /**
+ * Builds the [[ExpressionInfo]] of a class-based function (`CREATE FUNCTION
... AS 'class'`).
+ * A DataSource V2 function reports its own description, which requires
instantiating it; if the
+ * class is not on the classpath yet (its `USING` resources are only loaded
on first invocation)
+ * or cannot be instantiated, fall back to the class-name-only info, so that
like a Hive UDF, a
+ * broken function only fails when invoked. Other classes are checked
without being initialized.
+ */
+ private def makeExprInfoForClassFunction(func: CatalogFunction):
ExpressionInfo = {
+ if (Utils.classIsLoadableAndAssignableFrom(func.className,
classOf[UnboundFunction])) {
+ try {
+ val clazz = Utils.classForName(func.className)
+ makeExprInfoForV2Function(func, newV2FunctionInstance(clazz,
func.identifier))
+ } catch {
+ case NonFatal(_) => makeExprInfoForHiveFunction(func)
Review Comment:
**Non-blocking (P2):** This fallback only catches `NonFatal`, and `NonFatal`
treats `LinkageError` as fatal. The `classIsLoadableAndAssignableFrom` check at
line 2220 also sits outside the `try`, and its `Try` has the same gap. Before
this PR these metadata paths never touched the class. Now
`lookupPersistentFunction` and the eager info in `loadPersistentScalarFunction`
load it, and for a V2 class they also initialize and construct it. So a
persistent function whose class has a throwing static initializer
(`ExceptionInInitializerError`, then `NoClassDefFoundError: Could not
initialize class`) or a missing superclass (`NoClassDefFoundError`) now breaks
`DESCRIBE FUNCTION`, `spark.catalog.getFunction`, Thrift `getFunctions`, and
`spark.catalog.listFunctions()` for the whole database with a raw
`java.lang.Error`. That contradicts the fail-only-on-invoke behavior the PR
promises.
There is also an ordering regression. The eager info at line 2796 is
computed before `builderFactory` calls `loadFunctionResources`. Suppose the
class is already visible (say its jar was added earlier) but its superclass or
a static-initializer dependency only exists in a jar from this function's
`USING` clause. Then the lookup throws before the resources are ever loaded, so
the function can't be resolved in the session at all. The merge target loaded
resources first and resolved it fine. For a V2 class, initializing it this
early can also leave it permanently erroneous in the classloader.
**Recommended change:** Make class-function metadata construction safe for
broken classes and keep it from initializing a class before that function's
USING resources are loaded. In makeExprInfoForClassFunction, treat a
LinkageError from the loadability check, class initialization or construction
like any other non-fatal failure, and fall back to the class-name-only info. On
the metadata-only paths (DESCRIBE lookup and the eager info built before first
invocation), do not load or instantiate the class of a function that declares
USING resources; report the class-name-only info until first invocation loads
the resources and registers the function with its full info.
**Why this works:** With LinkageError in the fallback at the single info
producer, no metadata consumer can receive an escaping class-loading Error.
With the class left untouched for resource-bearing functions until
registration, the merge-target order is restored: USING resources load before
the class initializes. A dependency that only exists in those resources is then
present when the static initializer runs, and the shared classloader never
records a class as erroneous because of premature initialization.
**Scope:** Narrow the class-function ExpressionInfo builder in
SessionCatalog so its fallback covers linkage failures and it does not touch a
function's class on metadata paths before that function's USING resources are
loaded. Add tests for broken-class metadata operations and for USING-resource
ordering.
**Compatibility:** Hive UDF metadata stays class-name-only and Hive UDF
invocation is unchanged. DESCRIBE for a classpath V2 function without USING
resources still reports its description before first use.
**Risks:** Catching LinkageError must not also swallow VirtualMachineError
or other fatal throwables; those must keep propagating. A V2 function that
declares USING resources but whose class happens to be visible already will now
show class-name-only info until it is first invoked, so any expectation of an
earlier description for such functions must follow the accepted timing.
**Constraints:** On the invocation path, a function's USING resources must
still load before its class is initialized, as in the merge target.
External-catalog calls stay the same as for a Hive UDF.
**Exclusions:**
- Changing the shared class-loadability utility for its other callers.
- The codegen classloader question for magic-method functions loaded from
jars, which awaits the author's decision.
**Success:** DESCRIBE FUNCTION, spark.catalog.listFunctions,
spark.catalog.getFunction and JDBC getFunctions succeed with class-name-only
info for a persistent class-based function whose class fails to link or
initialize, and only invoking that function fails. No metadata lookup loads or
initializes the class of a persistent function before that function's USING
resources are loaded. A persistent V2 function without USING resources whose
class is on the classpath still shows its description in DESCRIBE before first
use.
**Verification:**
- **Regression:** For a persistent V2 function whose class has a throwing
static initializer, listing user functions, the catalog listing and lookup
APIs, and DESCRIBE FUNCTION EXTENDED all succeed with class-name-only info,
while invoking the function fails with an error.
- **Regression:** For a persistent function whose class is already visible
but whose superclass or static-initializer dependency exists only in a jar
listed in the function's USING clause, describing the function before first use
does not initialize the class, and the first invocation succeeds after the
resources load.
- **Behavior:** A persistent V2 function with no USING resources and a
classpath class reports its description in DESCRIBE before it is ever invoked,
and a USING JAR V2 function reports its description after first invocation.
- **Compatibility:** Existing persistent Hive UDFs keep class-name-only
metadata and resolve and run exactly as before, including when their USING
resources are loaded only on first invocation.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryCompilationErrors.scala:
##########
@@ -1442,6 +1442,17 @@ private[sql] object QueryCompilationErrors extends
QueryErrorsBase with Compilat
new FunctionAlreadyExistsException(func.nameParts)
}
+ def cannotInstantiateFunctionClassError(
+ className: String, func: FunctionIdentifier, cause: Throwable):
Throwable = {
+ new AnalysisException(
+ errorClass = "CANNOT_INSTANTIATE_FUNCTION_CLASS",
+ messageParameters = Map(
+ "className" -> className,
+ "functionName" -> toSQLId(func.toString),
+ "msg" -> Option(cause.getMessage).getOrElse("")),
Review Comment:
**Nit (P3):** `<msg>` comes from `cause.getMessage`, but the most likely
failure here is a public no-arg constructor that throws.
`Constructor.newInstance` wraps that in `InvocationTargetException`, whose
message is null. The user then sees `... please make sure it has a public
no-arg constructor. ` with nothing after it. That points at the wrong cause and
drops the real one; JDBC, Thrift and Connect clients only get this text.
Unwrapping the target exception in `newV2FunctionInstance`, and wording the
template so it also covers a constructor that throws, would make the message
accurate. The 'class without a public no-arg constructor' test only checks the
condition and `className` (and only the condition on the persistent path), so
it wouldn't catch this.
**Verification:**
- **Regression:** Registering a temporary V2 function, and first invoking a
persistent one, whose public no-arg constructor throws yields
CANNOT_INSTANTIATE_FUNCTION_CLASS whose message parameter contains the
constructor's own failure message.
- **Behavior:** A V2 class without a usable no-arg constructor yields
CANNOT_INSTANTIATE_FUNCTION_CLASS with every message parameter asserted,
including the function name for both the temporary and the persistent
registration.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/catalog/SessionCatalog.scala:
##########
@@ -2076,14 +2079,47 @@ class SessionCatalog(
* Constructs a [[FunctionBuilder]] based on the provided function metadata.
*/
private def makeFunctionBuilder(func: CatalogFunction): FunctionBuilder = {
+ val clazz = loadFunctionClass(func)
+ if (isV2FunctionClass(clazz)) {
+ // A DataSource V2 function: bind it to the arguments at each call site.
An aggregate
+ // function is returned as a bare `V2Aggregator`, which the caller wraps
into an
+ // `AggregateExpression` carrying DISTINCT / FILTER. This check goes
first so that a class
+ // also implementing a Hive UDF interface, for other engines sharing the
metastore, runs as
+ // a V2 function in Spark.
+ val unbound = newV2FunctionInstance(clazz, func.identifier)
+ (input: Seq[Expression]) => V2ExpressionUtils.bindFunction(unbound,
input)
Review Comment:
**Nit (P3):** The builder returns the raw `bindFunction` result
(`ApplyFunctionExpression`, `StaticInvoke`/`Invoke`, or a bare `V2Aggregator`)
without a `FUNC_ALIAS` tag, and none of these nodes overrides `prettyName`. So
when `validateFunction` rejects a clause on this V1 path, the error names the
node rather than the function. For example, `SELECT strlen(DISTINCT 'a')` on a
session-catalog V2 scalar reports `applyfunctionexpression` (or
`staticinvoke`/`invoke` for a magic method), and `iavg(x) IGNORE NULLS` reports
`v2aggregator`. FILTER and WITHIN GROUP behave the same way. Hive UDF wrappers
on this path override `prettyName` with the function name, and the
FunctionCatalog path passes `unbound.name()`, so the same class reports a
different name depending on where it is registered. The suite has no negative
test for these clauses.
Tagging the returned expression with `FunctionRegistry.FUNC_ALIAS` set to
the registered name would fix the errors. Note that it also changes the default
output column names of the `produceResult` and aggregate shapes, which render
through `prettyName`. The magic-method shapes render through their own
`toString`, so their column names stay as they are.
**Verification:**
- **Regression:** Temporary and persistent session-catalog V2 functions,
both scalar (magic-method and produceResult) and aggregate, reject each
unsupported clause with an error whose function-name parameter is the
registered function name rather than an internal node name.
- **Compatibility:** Unsupported-clause and type-check errors for V2
functions resolved through a FunctionCatalog keep their current function names
and SQL text.
- **Behavior:** Positive session-catalog V2 calls of each shape
(magic-method scalar, produceResult scalar, and aggregate with DISTINCT and
FILTER) return the same results after tagging, and their output column names
are asserted, so the column-name change for the produceResult and aggregate
shapes is deliberate.
--
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]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]