On Fri, Oct 25, 2024 at 10:55 PM <ma...@apache.org> wrote:
>
> This is an automated email from the ASF dual-hosted git repository.
>
> markt pushed a commit to branch main
> in repository https://gitbox.apache.org/repos/asf/tomcat.git
>
> commit 3b16fb8c1444e7ce76fb33bcd38378a4bb987c23
> Author: Mark Thomas <ma...@apache.org>
> AuthorDate: Fri Oct 25 21:54:12 2024 +0100
>
>     Cache class loader non-found resources
>
>     Some components (e.g. DriverManager) will attempt to load the same class
>     over and over again even if it isn't found the first time. If the web
>     application is large, this search can take a noticeable amount of time.
>     This cache avoids those issues. It can be disabled if not wanted /
>     required.

I know that, this existed a long time ago:
https://github.com/apache/tomcat/blob/7.0.x/java/org/apache/catalina/loader/WebappClassLoaderBase.java#L341
It was quite significant then since everything was much slower.

Rémy

> ---
>  java/org/apache/catalina/core/StandardContext.java |  13 ++
>  .../catalina/loader/WebappClassLoaderBase.java     | 139 
> +++++++++++++--------
>  .../util/collections/ConcurrentLruCache.java       | 119 ++++++++++++++++++
>  .../util/collections/TestConcurrentLruCache.java   |  96 ++++++++++++++
>  webapps/docs/config/context.xml                    |  11 ++
>  5 files changed, 327 insertions(+), 51 deletions(-)
>
> diff --git a/java/org/apache/catalina/core/StandardContext.java 
> b/java/org/apache/catalina/core/StandardContext.java
> index f4fb6f8bf0..2994082059 100644
> --- a/java/org/apache/catalina/core/StandardContext.java
> +++ b/java/org/apache/catalina/core/StandardContext.java
> @@ -798,9 +798,21 @@ public class StandardContext extends ContainerBase 
> implements Context, Notificat
>
>      private boolean parallelAnnotationScanning = false;
>
> +    private int notFoundClassResourceCacheSize = 1000;
> +
>
>      // ----------------------------------------------------- Context 
> Properties
>
> +    public int getNotFoundClassResourceCacheSize() {
> +        return notFoundClassResourceCacheSize;
> +    }
> +
> +
> +    public void setNotFoundClassResourceCacheSize(int 
> notFoundClassResourceCacheSize) {
> +        this.notFoundClassResourceCacheSize = notFoundClassResourceCacheSize;
> +    }
> +
> +
>      @Override
>      public void setCreateUploadTargets(boolean createUploadTargets) {
>          this.createUploadTargets = createUploadTargets;
> @@ -4258,6 +4270,7 @@ public class StandardContext extends ContainerBase 
> implements Context, Notificat
>                      
> cl.setClearReferencesHttpClientKeepAliveThread(getClearReferencesHttpClientKeepAliveThread());
>                      
> cl.setClearReferencesThreadLocals(getClearReferencesThreadLocals());
>                      
> cl.setSkipMemoryLeakChecksOnJvmShutdown(getSkipMemoryLeakChecksOnJvmShutdown());
> +                    
> cl.setNotFoundClassResourceCacheSize(getNotFoundClassResourceCacheSize());
>                  }
>
>                  // By calling unbindThread and bindThread in a row, we setup 
> the
> diff --git a/java/org/apache/catalina/loader/WebappClassLoaderBase.java 
> b/java/org/apache/catalina/loader/WebappClassLoaderBase.java
> index eb41edfeeb..2164378b47 100644
> --- a/java/org/apache/catalina/loader/WebappClassLoaderBase.java
> +++ b/java/org/apache/catalina/loader/WebappClassLoaderBase.java
> @@ -67,6 +67,7 @@ import org.apache.juli.logging.LogFactory;
>  import org.apache.tomcat.InstrumentableClassLoader;
>  import org.apache.tomcat.util.ExceptionUtils;
>  import org.apache.tomcat.util.IntrospectionUtils;
> +import org.apache.tomcat.util.collections.ConcurrentLruCache;
>  import org.apache.tomcat.util.compat.JreCompat;
>  import org.apache.tomcat.util.res.StringManager;
>  import org.apache.tomcat.util.threads.ThreadPoolExecutor;
> @@ -311,9 +312,26 @@ public abstract class WebappClassLoaderBase extends 
> URLClassLoader
>
>      private volatile LifecycleState state = LifecycleState.NEW;
>
> +    /*
> +     * Class resources are not cached since they are loaded on first use and 
> the resource is then no longer required.
> +     * It does help, however, to cache classes that are not found as in some 
> scenarios the same class will be searched
> +     * for many times and the greater the number of JARs/classes, the longer 
> that lookup will take.
> +     */
> +    private final ConcurrentLruCache<String> notFoundClassResources = new 
> ConcurrentLruCache<>(1000);
> +
>
>      // ------------------------------------------------------------- 
> Properties
>
> +    public void setNotFoundClassResourceCacheSize(int 
> notFoundClassResourceCacheSize) {
> +        notFoundClassResources.setLimit(notFoundClassResourceCacheSize);
> +    }
> +
> +
> +    public int getNotFoundClassResourceCacheSize() {
> +        return notFoundClassResources.getLimit();
> +    }
> +
> +
>      /**
>       * Set associated resources.
>       *
> @@ -684,14 +702,20 @@ public abstract class WebappClassLoaderBase extends 
> URLClassLoader
>
>          String path = nameToPath(name);
>
> -        WebResource resource = resources.getClassLoaderResource(path);
> -        if (resource.exists()) {
> -            url = resource.getURL();
> -            trackLastModified(path, resource);
> -        }
> +        if (!notFoundClassResources.contains(path)) {
> +            WebResource resource = resources.getClassLoaderResource(path);
> +            if (resource.exists()) {
> +                url = resource.getURL();
> +                trackLastModified(path, resource);
> +            }
> +
> +            if (url == null && hasExternalRepositories) {
> +                url = super.findResource(name);
> +            }
>
> -        if (url == null && hasExternalRepositories) {
> -            url = super.findResource(name);
> +            if (url == null) {
> +                notFoundClassResources.add(path);
> +            }
>          }
>
>          if (log.isTraceEnabled()) {
> @@ -878,60 +902,64 @@ public abstract class WebappClassLoaderBase extends 
> URLClassLoader
>              log.trace("  Searching local repositories");
>          }
>          String path = nameToPath(name);
> -        WebResource resource = resources.getClassLoaderResource(path);
> -        if (resource.exists()) {
> -            stream = resource.getInputStream();
> -            // Filter out .class resources through the ClassFileTranformer
> -            if (name.endsWith(CLASS_FILE_SUFFIX) && transformers.size() > 0) 
> {
> -                // If the resource is a class, decorate it with any attached 
> transformers
> -                ByteArrayOutputStream baos = new ByteArrayOutputStream();
> -                byte[] buf = new byte[8192];
> -                int numRead;
> -                try {
> -                    while ((numRead = stream.read(buf)) >= 0) {
> -                        baos.write(buf, 0, numRead);
> -                    }
> -                } catch (IOException e) {
> -                    
> log.error(sm.getString("webappClassLoader.transformError", name), e);
> -                    return null;
> -                } finally {
> -                    try {
> -                        stream.close();
> -                    } catch (IOException e) {
> -                    }
> -                }
> -                byte[] binaryContent = baos.toByteArray();
> -                String internalName = path.substring(1, path.length() - 
> CLASS_FILE_SUFFIX.length());
> -                for (ClassFileTransformer transformer : this.transformers) {
> +        if (!notFoundClassResources.contains(path)) {
> +            WebResource resource = resources.getClassLoaderResource(path);
> +            if (resource.exists()) {
> +                stream = resource.getInputStream();
> +                // Filter out .class resources through the 
> ClassFileTranformer
> +                if (name.endsWith(CLASS_FILE_SUFFIX) && transformers.size() 
> > 0) {
> +                    // If the resource is a class, decorate it with any 
> attached transformers
> +                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
> +                    byte[] buf = new byte[8192];
> +                    int numRead;
>                      try {
> -                        byte[] transformed = transformer.transform(this, 
> internalName, null, null, binaryContent);
> -                        if (transformed != null) {
> -                            binaryContent = transformed;
> +                        while ((numRead = stream.read(buf)) >= 0) {
> +                            baos.write(buf, 0, numRead);
>                          }
> -                    } catch (IllegalClassFormatException e) {
> +                    } catch (IOException e) {
>                          
> log.error(sm.getString("webappClassLoader.transformError", name), e);
>                          return null;
> +                    } finally {
> +                        try {
> +                            stream.close();
> +                        } catch (IOException e) {
> +                        }
> +                    }
> +                    byte[] binaryContent = baos.toByteArray();
> +                    String internalName = path.substring(1, path.length() - 
> CLASS_FILE_SUFFIX.length());
> +                    for (ClassFileTransformer transformer : 
> this.transformers) {
> +                        try {
> +                            byte[] transformed = transformer.transform(this, 
> internalName, null, null, binaryContent);
> +                            if (transformed != null) {
> +                                binaryContent = transformed;
> +                            }
> +                        } catch (IllegalClassFormatException e) {
> +                            
> log.error(sm.getString("webappClassLoader.transformError", name), e);
> +                            return null;
> +                        }
>                      }
> +                    stream = new ByteArrayInputStream(binaryContent);
>                  }
> -                stream = new ByteArrayInputStream(binaryContent);
> +                trackLastModified(path, resource);
>              }
> -            trackLastModified(path, resource);
> -        }
> -        try {
> -            if (hasExternalRepositories && stream == null) {
> -                URL url = super.findResource(name);
> -                if (url != null) {
> -                    stream = url.openStream();
> +            try {
> +                if (hasExternalRepositories && stream == null) {
> +                    URL url = super.findResource(name);
> +                    if (url != null) {
> +                        stream = url.openStream();
> +                    }
>                  }
> +            } catch (IOException e) {
> +                // Ignore
>              }
> -        } catch (IOException e) {
> -            // Ignore
> -        }
> -        if (stream != null) {
> -            if (log.isTraceEnabled()) {
> -                log.trace("  --> Returning stream from local");
> +            if (stream != null) {
> +                if (log.isTraceEnabled()) {
> +                    log.trace("  --> Returning stream from local");
> +                }
> +                return stream;
>              }
> -            return stream;
> +
> +            notFoundClassResources.add(path);
>          }
>
>          // (3) Delegate to parent unconditionally
> @@ -1263,6 +1291,7 @@ public abstract class WebappClassLoaderBase extends 
> URLClassLoader
>          state = LifecycleState.STOPPING;
>
>          resourceEntries.clear();
> +        notFoundClassResources.clear();
>          jarModificationTimes.clear();
>          resources = null;
>
> @@ -1964,9 +1993,13 @@ public abstract class WebappClassLoaderBase extends 
> URLClassLoader
>          WebResource resource = null;
>
>          if (entry == null) {
> +            if (notFoundClassResources.contains(path)) {
> +                return null;
> +            }
>              resource = resources.getClassLoaderResource(path);
>
>              if (!resource.exists()) {
> +                notFoundClassResources.add(path);
>                  return null;
>              }
>
> @@ -1999,10 +2032,14 @@ public abstract class WebappClassLoaderBase extends 
> URLClassLoader
>              }
>
>              if (resource == null) {
> +                if (notFoundClassResources.contains(path)) {
> +                    return null;
> +                }
>                  resource = resources.getClassLoaderResource(path);
>              }
>
>              if (!resource.exists()) {
> +                notFoundClassResources.add(path);
>                  return null;
>              }
>
> diff --git a/java/org/apache/tomcat/util/collections/ConcurrentLruCache.java 
> b/java/org/apache/tomcat/util/collections/ConcurrentLruCache.java
> new file mode 100644
> index 0000000000..51e29b969c
> --- /dev/null
> +++ b/java/org/apache/tomcat/util/collections/ConcurrentLruCache.java
> @@ -0,0 +1,119 @@
> +/*
> + * Licensed to the Apache Software Foundation (ASF) under one or more
> + * contributor license agreements.  See the NOTICE file distributed with
> + * this work for additional information regarding copyright ownership.
> + * The ASF licenses this file to You under the Apache License, Version 2.0
> + * (the "License"); you may not use this file except in compliance with
> + * the License.  You may obtain a copy of the License at
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +package org.apache.tomcat.util.collections;
> +
> +import java.util.LinkedHashMap;
> +import java.util.Map;
> +
> +public class ConcurrentLruCache<T> {
> +
> +    private volatile LimitedLinkedHashMap<T,T> map;
> +    private final Object lock = new Object();
> +
> +    public ConcurrentLruCache(int limit) {
> +        setLimit(limit);
> +    }
> +
> +
> +    public void add(T entry) {
> +        if (map == null) {
> +            return;
> +        }
> +        synchronized (lock) {
> +            if (map == null) {
> +                return;
> +            }
> +            map.put(entry, entry);
> +        }
> +    }
> +
> +
> +    public boolean contains(T entry) {
> +        if (map == null) {
> +            return false;
> +        }
> +        synchronized (lock) {
> +            if (map == null) {
> +                return false;
> +            }
> +            return map.get(entry) != null;
> +        }
> +    }
> +
> +
> +    public void clear() {
> +        if (map == null) {
> +            return;
> +        }
> +        synchronized (lock) {
> +            if (map == null) {
> +                return;
> +            }
> +            map.clear();
> +        }
> +    }
> +
> +
> +    public void setLimit(int limit) {
> +        synchronized (lock) {
> +            if (limit > 0) {
> +                Map<T,T> oldMap = map;
> +                map = new LimitedLinkedHashMap<>(limit);
> +                if (oldMap != null) {
> +                    map.putAll(oldMap);
> +                }
> +           } else {
> +                map = null;
> +            }
> +        }
> +    }
> +
> +
> +    public int getLimit() {
> +        synchronized (lock) {
> +            if (map == null) {
> +                return -1;
> +            } else {
> +                return map.getLimit();
> +            }
> +        }
> +    }
> +
> +
> +    private static class LimitedLinkedHashMap<K,V> extends 
> LinkedHashMap<K,V> {
> +        private static final long serialVersionUID = 1L;
> +
> +        private volatile int limit;
> +
> +        LimitedLinkedHashMap(int limit) {
> +            super(16, 0.75F, true);
> +            this.limit = limit;
> +        }
> +
> +        @Override
> +        protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
> +            if (size() > limit) {
> +                return true;
> +            }
> +            return false;
> +        }
> +
> +        private int getLimit() {
> +            return limit;
> +        }
> +    }
> +}
> diff --git 
> a/test/org/apache/tomcat/util/collections/TestConcurrentLruCache.java 
> b/test/org/apache/tomcat/util/collections/TestConcurrentLruCache.java
> new file mode 100644
> index 0000000000..94fa447903
> --- /dev/null
> +++ b/test/org/apache/tomcat/util/collections/TestConcurrentLruCache.java
> @@ -0,0 +1,96 @@
> +/*
> + * Licensed to the Apache Software Foundation (ASF) under one or more
> + * contributor license agreements.  See the NOTICE file distributed with
> + * this work for additional information regarding copyright ownership.
> + * The ASF licenses this file to You under the Apache License, Version 2.0
> + * (the "License"); you may not use this file except in compliance with
> + * the License.  You may obtain a copy of the License at
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +package org.apache.tomcat.util.collections;
> +
> +import org.junit.Assert;
> +import org.junit.Test;
> +
> +public class TestConcurrentLruCache {
> +
> +    @Test
> +    public void testLimit() {
> +        ConcurrentLruCache<String> cache = getStartingCache();
> +
> +        cache.add("D");
> +
> +        Assert.assertFalse(cache.contains("A"));
> +        Assert.assertTrue(cache.contains("B"));
> +        Assert.assertTrue(cache.contains("C"));
> +        Assert.assertTrue(cache.contains("D"));
> +    }
> +
> +
> +    @Test
> +    public void testLRU() {
> +        ConcurrentLruCache<String> cache = getStartingCache();
> +
> +        cache.contains("A");
> +        cache.add("D");
> +
> +        Assert.assertTrue(cache.contains("A"));
> +        Assert.assertFalse(cache.contains("B"));
> +        Assert.assertTrue(cache.contains("C"));
> +        Assert.assertTrue(cache.contains("D"));
> +    }
> +
> +
> +    @Test
> +    public void testIncreaseLimit() {
> +        ConcurrentLruCache<String> cache = getStartingCache();
> +
> +        cache.setLimit(4);
> +
> +        cache.add("D");
> +
> +        Assert.assertTrue(cache.contains("A"));
> +        Assert.assertTrue(cache.contains("B"));
> +        Assert.assertTrue(cache.contains("C"));
> +        Assert.assertTrue(cache.contains("D"));
> +
> +        cache.add("E");
> +
> +        Assert.assertFalse(cache.contains("A"));
> +        Assert.assertTrue(cache.contains("B"));
> +        Assert.assertTrue(cache.contains("C"));
> +        Assert.assertTrue(cache.contains("D"));
> +        Assert.assertTrue(cache.contains("E"));
> +    }
> +
> +
> +    @Test
> +    public void testReduceLimit() {
> +        ConcurrentLruCache<String> cache = getStartingCache();
> +
> +        cache.contains("A");
> +        cache.setLimit(2);
> +
> +        Assert.assertTrue(cache.contains("A"));
> +        Assert.assertFalse(cache.contains("B"));
> +        Assert.assertTrue(cache.contains("C"));
> +    }
> +
> +
> +    private ConcurrentLruCache<String> getStartingCache() {
> +        ConcurrentLruCache<String> cache = new ConcurrentLruCache<>(3);
> +
> +        cache.add("A");
> +        cache.add("B");
> +        cache.add("C");
> +
> +        return cache;
> +    }
> +}
> diff --git a/webapps/docs/config/context.xml b/webapps/docs/config/context.xml
> index 9e59e39bb9..b141edc8dc 100644
> --- a/webapps/docs/config/context.xml
> +++ b/webapps/docs/config/context.xml
> @@ -870,6 +870,17 @@
>          of <code>true</code> will be used.</p>
>        </attribute>
>
> +      <attribute name="notFoundClassResourceCacheSize" required="false">
> +        <p>Class resources are not cached by the standard resources
> +        implementation since they are loaded on first use and then the 
> resource
> +        is then no longer required. It does help, however, to cache classes 
> that
> +        are not found as in some scenarios the same class will be searched 
> for
> +        many times and the greater the number of JARs/classes, the longer 
> that
> +        search will take. An LRU cache is used and this attribute controls 
> the
> +        size of that cache. A value of less than 1 disables the cache. If not
> +        specified, the default value of 1000 will be used.</p>
> +      </attribute>
> +
>        <attribute name="renewThreadsWhenStoppingContext" required="false">
>          <p>If <code>true</code>, when this context is stopped, Tomcat renews 
> all
>          the threads from the thread pool that was used to serve this context.
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
> For additional commands, e-mail: dev-h...@tomcat.apache.org
>

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to