Skip to content

Commit 5c97bb5

Browse files
branch-4.0: [bug](udf) udf should cache classloader in static load #60709 (#60975)
Cherry-picked from #60709 Co-authored-by: zhangstar333 <[email protected]>
1 parent bd23cb6 commit 5c97bb5

File tree

5 files changed

+54
-33
lines changed

5 files changed

+54
-33
lines changed

fe/be-java-extensions/java-common/src/main/java/org/apache/doris/common/jni/utils/ExpiringMap.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ public void put(K key, V value, long expirationTimeMs) {
4747
public V get(K key) {
4848
Long expirationTime = expirationMap.get(key);
4949
if (expirationTime == null || System.currentTimeMillis() > expirationTime) {
50-
map.remove(key);
51-
expirationMap.remove(key);
52-
ttlMap.remove(key);
50+
remove(key);
5351
return null;
5452
}
5553
// reset time again
@@ -64,18 +62,25 @@ private void startExpirationTask() {
6462
long now = System.currentTimeMillis();
6563
for (K key : expirationMap.keySet()) {
6664
if (expirationMap.get(key) <= now) {
67-
map.remove(key);
68-
expirationMap.remove(key);
69-
ttlMap.remove(key);
65+
remove(key);
7066
}
7167
}
7268
}, DEFAULT_INTERVAL_TIME, DEFAULT_INTERVAL_TIME, TimeUnit.MINUTES);
7369
}
7470

7571
public void remove(K key) {
76-
map.remove(key);
72+
V value = map.remove(key);
7773
expirationMap.remove(key);
7874
ttlMap.remove(key);
75+
76+
// Uniformly release resources for any AutoCloseable value,
77+
if (value instanceof AutoCloseable) {
78+
try {
79+
((AutoCloseable) value).close();
80+
} catch (Exception e) {
81+
LOG.warn("Failed to close cached resource: " + key, e);
82+
}
83+
}
7984
}
8085

8186
public int size() {

fe/be-java-extensions/java-common/src/main/java/org/apache/doris/common/jni/utils/UdfClassCache.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
package org.apache.doris.common.jni.utils;
1919

2020
import com.esotericsoftware.reflectasm.MethodAccess;
21+
import org.apache.log4j.Logger;
2122

23+
import java.io.IOException;
2224
import java.lang.reflect.Method;
25+
import java.net.URLClassLoader;
2326
import java.util.HashMap;
2427

2528
/**
2629
* This class is used for caching the class of UDF.
2730
*/
28-
public class UdfClassCache {
31+
public class UdfClassCache implements AutoCloseable {
32+
private static final Logger LOG = Logger.getLogger(UdfClassCache.class);
2933
public Class<?> udfClass;
3034
// the index of evaluate() method in the class
3135
public MethodAccess methodAccess;
@@ -42,4 +46,22 @@ public class UdfClassCache {
4246
// for java-udf index is evaluate method index
4347
// for java-udaf index is add method index
4448
public int methodIndex;
49+
50+
// Keep a reference to the ClassLoader for static load mode
51+
// This ensures the ClassLoader is not garbage collected and can load dependent classes
52+
// Note: classLoader may be null when jarPath is empty (UDF loaded from custom_lib via
53+
// system class loader), which must not be closed — null is intentional in that case.
54+
public URLClassLoader classLoader;
55+
56+
@Override
57+
public void close() {
58+
if (classLoader != null) {
59+
try {
60+
classLoader.close();
61+
} catch (IOException e) {
62+
LOG.warn("Failed to close ClassLoader", e);
63+
}
64+
classLoader = null;
65+
}
66+
}
4567
}

fe/be-java-extensions/java-udf/src/main/java/org/apache/doris/udf/BaseExecutor.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.apache.thrift.protocol.TBinaryProtocol;
4141

4242
import java.io.FileNotFoundException;
43-
import java.io.IOException;
4443
import java.lang.reflect.Constructor;
4544
import java.net.MalformedURLException;
4645
import java.net.URLClassLoader;
@@ -139,6 +138,10 @@ public UdfClassCache getClassCache(String jarPath, String signature, long expira
139138
UdfClassCache cache = null;
140139
if (isStaticLoad) {
141140
cache = ScannerLoader.getUdfClassLoader(signature);
141+
if (cache != null && cache.classLoader != null) {
142+
// Reuse the cached classLoader to ensure dependent classes can be loaded
143+
classLoader = cache.classLoader;
144+
}
142145
}
143146
if (cache == null) {
144147
ClassLoader loader;
@@ -156,6 +159,7 @@ public UdfClassCache getClassCache(String jarPath, String signature, long expira
156159
cache.allMethods = new HashMap<>();
157160
cache.udfClass = Class.forName(className, true, loader);
158161
cache.methodAccess = MethodAccess.get(cache.udfClass);
162+
cache.classLoader = classLoader;
159163
checkAndCacheUdfClass(cache, funcRetType, parameterTypes);
160164
if (isStaticLoad) {
161165
ScannerLoader.cacheClassLoader(signature, cache, expirationTime);
@@ -171,24 +175,17 @@ protected abstract void checkAndCacheUdfClass(UdfClassCache cache, Type funcRetT
171175
* Close the class loader we may have created.
172176
*/
173177
public void close() {
174-
if (classLoader != null) {
175-
try {
176-
classLoader.close();
177-
} catch (IOException e) {
178-
// Log and ignore.
179-
if (LOG.isDebugEnabled()) {
180-
LOG.debug("Error closing the URLClassloader.", e);
181-
}
182-
}
183-
}
184178
// Close the output table if it exists.
185179
if (outputTable != null) {
186180
outputTable.close();
187181
}
188-
// We are now un-usable (because the class loader has been
189-
// closed), so null out method_ and classLoader_.
190-
classLoader = null;
191-
objCache.methodAccess = null;
182+
if (!isStaticLoad) {
183+
// close classLoader via UdfClassCache.close() if not in static load mode.
184+
// In static load mode, the classLoader is cached and should not be closed here.
185+
objCache.close();
186+
objCache.methodAccess = null;
187+
classLoader = null;
188+
}
192189
}
193190

194191
protected ColumnValueConverter getInputConverter(TPrimitiveType primitiveType, Class clz)

fe/be-java-extensions/java-udf/src/main/java/org/apache/doris/udf/UdafExecutor.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ public UdafExecutor(byte[] thriftParams) throws Exception {
7272
*/
7373
@Override
7474
public void close() {
75-
if (!isStaticLoad) {
76-
super.close();
77-
}
75+
// Call parent's close method which handles classLoader and outputTable properly
76+
// It will only close classLoader if not in static load mode
77+
super.close();
78+
// Clear the state map
7879
stateObjMap = null;
7980
}
8081

fe/be-java-extensions/java-udf/src/main/java/org/apache/doris/udf/UdfExecutor.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,9 @@ public UdfExecutor(byte[] thriftParams) throws Exception {
5555
*/
5656
@Override
5757
public void close() {
58-
// We are now un-usable (because the class loader has been
59-
// closed), so null out method_ and classLoader_.
60-
if (!isStaticLoad) {
61-
super.close();
62-
} else if (outputTable != null) {
63-
outputTable.close();
64-
}
58+
// Call parent's close method which handles classLoader properly
59+
// It will only close classLoader if not in static load mode
60+
super.close();
6561
}
6662

6763
public long evaluate(Map<String, String> inputParams, Map<String, String> outputParams) throws UdfRuntimeException {

0 commit comments

Comments
 (0)