@@ -97,12 +97,14 @@ public class ScannerLoader {
9797 // 2) rebuilding a fresh URLClassLoader on every eviction produced multiple coexisting
9898 // ClassLoaders for the same UDF, which broke lazy class resolution and reflective
9999 // lookups inside user UDF code.
100+ // Cache by function id so a recreated function with the same signature does not reuse
101+ // the previous function's class loader.
100102 // NOTE: a cache miss in BaseExecutor.getClassCache() is NOT only reachable after
101- // cleanUdfClassLoader() — concurrent first-time loads of the same signature can also
103+ // cleanUdfClassLoader() — concurrent first-time loads of the same function can also
102104 // both observe a miss. cacheClassLoader() must therefore insert atomically via
103105 // putIfAbsent and must never close a cache that was already published to the map,
104106 // because another executor may already be holding it.
105- private static final Map <String , UdfClassCache > udfLoadedClasses = new ConcurrentHashMap <>();
107+ private static final Map <Long , UdfClassCacheEntry > udfLoadedClasses = new ConcurrentHashMap <>();
106108 private static final String CLASS_SUFFIX = ".class" ;
107109 private static final String LOAD_PACKAGE = "org.apache.doris" ;
108110
@@ -126,61 +128,93 @@ public void loadAllScannerJars() {
126128 LOG .info ("Finished loading scanner JARs" );
127129 }
128130
129- public static UdfClassCache getUdfClassLoader (String functionSignature ) {
130- return udfLoadedClasses .get (functionSignature );
131+ private static class UdfClassCacheEntry {
132+ private final String functionSignature ;
133+ private final UdfClassCache classCache ;
134+
135+ UdfClassCacheEntry (String functionSignature , UdfClassCache classCache ) {
136+ this .functionSignature = functionSignature ;
137+ this .classCache = classCache ;
138+ }
139+ }
140+
141+ public static UdfClassCache getUdfClassLoader (long functionId ) {
142+ UdfClassCacheEntry entry = udfLoadedClasses .get (functionId );
143+ return entry == null ? null : entry .classCache ;
131144 }
132145
133146 /**
134- * Cache the UDF class metadata for the given function signature .
147+ * Cache the UDF class metadata for the given catalog function id .
135148 *
136- * <p>Insertion is atomic via {@link Map#putIfAbsent}: if another executor thread has
137- * already published a cache entry for {@code functionSignature }, the {@code classCache}
149+ * <p>Insertion is atomic via {@link Map#putIfAbsent}: if another executor
150+ * thread has already published a cache entry for {@code functionId }, the {@code classCache}
138151 * argument is treated as a redundant build and closed here (it has not yet been handed
139152 * to any executor, so closing its URLClassLoader is safe). The already-published entry
140153 * is returned to the caller so the current executor can switch to it.</p>
141154 *
142155 * <p>The {@code expirationTime} parameter is kept for backward compatibility with the
143156 * existing call sites and DDL property {@code expiration_time}, but is no longer used:
144157 * cached entries are not evicted by time. Removal happens only via
145- * {@link #cleanUdfClassLoader(String)} on DROP FUNCTION.</p>
158+ * {@link #cleanUdfClassLoader(String, long )} on DROP FUNCTION.</p>
146159 *
147160 * @return the {@link UdfClassCache} actually held in the map after this call —
148161 * either {@code classCache} (we won the race) or the pre-existing entry
149162 * (another thread won; {@code classCache} has been closed and must not be used).
150163 */
151- public static UdfClassCache cacheClassLoader (String functionSignature , UdfClassCache classCache ,
152- long expirationTime ) {
153- LOG .info ("Cache UDF for: " + functionSignature );
154- UdfClassCache existing = udfLoadedClasses .putIfAbsent (functionSignature , classCache );
164+ public static UdfClassCache cacheClassLoader (String functionSignature , long functionId ,
165+ UdfClassCache classCache , long expirationTime ) {
166+ LOG .info ("Cache UDF for function signature: {}, function id: {}" , functionSignature , functionId );
167+ UdfClassCacheEntry newEntry = new UdfClassCacheEntry (functionSignature , classCache );
168+ UdfClassCacheEntry existing = udfLoadedClasses .putIfAbsent (functionId , newEntry );
155169 if (existing == null ) {
156170 return classCache ;
157171 }
158172 // Lost the race against a concurrent first-time load. The cache we just built has
159173 // never been exposed to any executor, so closing its URLClassLoader here cannot
160174 // affect anyone. Do NOT touch `existing` — another executor may already be using it.
161175 try {
162- classCache .close ();
176+ newEntry . classCache .close ();
163177 } catch (Exception e ) {
164- LOG .warn ("Failed to close redundant UdfClassCache for " + functionSignature , e );
178+ LOG .warn ("Failed to close UdfClassCache for function signature: {}, function id: {}" ,
179+ newEntry .functionSignature , functionId , e );
165180 }
166- return existing ;
181+ return existing . classCache ;
167182 }
168183
169- public void cleanUdfClassLoader (String functionSignature ) {
170- LOG .info ("cleanUdfClassLoader for: " + functionSignature );
171- UdfClassCache removed = udfLoadedClasses .remove (functionSignature );
172- if (removed != null ) {
173- // Immediately close the URLClassLoader. NOTE: any in-flight query still holding a
174- // reference to this cache (e.g. via JNIContext.executor) will fail with
175- // NoClassDefFoundError on lazy class resolution after this point. This is the
176- // accepted semantic of DROP FUNCTION: the function is gone, queries against it
177- // are expected to fail.
178- try {
179- removed .close ();
180- } catch (Exception e ) {
181- LOG .warn ("Failed to close UdfClassCache for " + functionSignature , e );
184+ public void cleanUdfClassLoader (String functionSignature , long functionId ) {
185+ LOG .info ("cleanUdfClassLoader for function signature: {}, function id: {}" ,
186+ functionSignature , functionId );
187+ if (functionId > 0 ) {
188+ UdfClassCacheEntry removed = udfLoadedClasses .remove (functionId );
189+ if (removed != null ) {
190+ // Immediately close the URLClassLoader. NOTE: any in-flight query still holding a
191+ // reference to this cache (e.g. via JNIContext.executor) will fail with
192+ // NoClassDefFoundError on lazy class resolution after this point. This is the
193+ // accepted semantic of DROP FUNCTION: the function is gone, queries against it
194+ // are expected to fail.
195+ try {
196+ removed .classCache .close ();
197+ } catch (Exception e ) {
198+ LOG .warn ("Failed to close UdfClassCache for function signature: {}, function id: {}" ,
199+ removed .functionSignature , functionId , e );
200+ }
182201 }
202+ return ;
183203 }
204+
205+ // Old FEs do not set function_id in cleanup requests, so remove every cache with
206+ // the requested signature.
207+ udfLoadedClasses .forEach ((cachedFunctionId , entry ) -> {
208+ if (entry .functionSignature .equals (functionSignature )
209+ && udfLoadedClasses .remove (cachedFunctionId , entry )) {
210+ try {
211+ entry .classCache .close ();
212+ } catch (Exception e ) {
213+ LOG .warn ("Failed to close UdfClassCache for function signature: {}, function id: {}" ,
214+ entry .functionSignature , cachedFunctionId , e );
215+ }
216+ }
217+ });
184218 }
185219
186220 /**
0 commit comments