From b3fa588f33fcb17da9931eac4e1e4ca68138083b Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 3 Dec 2025 18:34:16 +0000 Subject: [PATCH 01/15] Add tools for ContextDefinition create and cleanup Modified ContextDefinition to implement KeywordExecutable so that the user can use `accumulo create-context-definition` to create the json required for the ContextDefinition file. Added a Cleaner to call `URLClassLoader.close` on instances that are about to be garbage collected. The LocalCachingContextCleaner is used to register the URLClassLoader with the Cleaner and this class also keeps references to the URLClassLoaders. The list is accessed from a MXBean so that a user can use JMX to retrieve a list of files that are referenced in the local cache directory from the classloaders that have been created but not yet gc'd. This should aid users in determining which files can be removed from the local cache directory. --- modules/local-caching-classloader/README.md | 16 +++- modules/local-caching-classloader/pom.xml | 11 +++ .../accumulo/classloader/lcc/Constants.java | 2 +- .../classloader/lcc/LocalCachingContext.java | 1 + ...LocalCachingContextClassLoaderFactory.java | 19 +++++ .../lcc/LocalCachingContextCleaner.java | 55 ++++++++++++ .../lcc/definition/ContextDefinition.java | 54 +++++++++++- .../lcc/jmx/ContextClassLoaders.java | 43 ++++++++++ .../lcc/jmx/ContextClassLoadersMXBean.java | 34 ++++++++ ...pache.accumulo.start.spi.KeywordExecutable | 20 +++++ ...lCachingContextClassLoaderFactoryTest.java | 4 +- ...AccumuloClusterClassLoaderFactoryTest.java | 83 +++++++++++++++++-- pom.xml | 4 +- 13 files changed, 329 insertions(+), 17 deletions(-) create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoaders.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoadersMXBean.java create mode 100644 modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index f0f6d2e..c870310 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -49,9 +49,12 @@ as changes are noticed based on the monitoring interval. An example of the conte ## Creating a ContextDefinition file -Users may take advantage of the `ContextDefinition.create` method to construct a ContextDefinition object. This -will calculate the checksums of the classpath elements. `ContextDefinition.toJson` can be used to serialize the -ContextDefinition to a file. +After the local-caching-classloader jar is built and placed into the $ACCUMULO_HOME/lib directory, then +the `$ACCUMULO_HOME/bin/accumulo create-context-definition` command can be used to create the +ContextDefinition json. The command takes 3 arguments: the name of the context, the monitor interval, +and a list of file URLs. The resulting json is printed to stdout and can be redirected to a file. Users +may take advantage of the `ContextDefinition.create` and `ContextDefinition.toJson` methods to +construct a ContextDefinition object if they wish to do this programmatically. ## Updating a ContextDefinition file @@ -90,7 +93,12 @@ create the classloader and return the exception to the calling code. Because the cache directory is shared among multiple processes, and one process can't know what the other processes are doing, this class cannot clean up the shared cache directory. It is left to the user to remove unused context cache directories -and unused old files within a context cache directory. +and unused old files within a context cache directory. To aid in this task a JMX MXBean has been created to expose the +files that are still referenced by the classloaders that are created. For an example of how to use this MXBean, please +see the test method `MiniAccumuloClusterClassLoaderFactoryTest.getReferencedFiles`. This method attaches to the +local Accumulo JVM processes to get the set of referenced files. It should be safe to delete files that are located +in the base cache directory (set by property `general.custom.classloader.lcc.cache.dir`) that are NOT in the set +of referenced files. ## Accumulo Configuration diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 8b39c34..397c79c 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -51,6 +51,11 @@ spotbugs-annotations true + + com.beust + jcommander + provided + com.github.ben-manes.caffeine caffeine @@ -82,6 +87,12 @@ accumulo-core provided + + + org.apache.accumulo + accumulo-start + provided + org.apache.hadoop hadoop-client-api diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/Constants.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/Constants.java index de0a49f..4ac4e04 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/Constants.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/Constants.java @@ -36,7 +36,7 @@ public class Constants { Property.GENERAL_ARBITRARY_PROP_PREFIX.getKey() + "classloader.lcc.update.grace.minutes"; public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(0); - public static final Gson GSON = new GsonBuilder().disableJdkUnsafe().create(); + public static final Gson GSON = new GsonBuilder().disableJdkUnsafe().setPrettyPrinting().create(); public static DigestUtils getChecksummer() { return new DigestUtils("SHA256"); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContext.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContext.java index 43184f4..eb86cab 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContext.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContext.java @@ -261,6 +261,7 @@ public ClassLoader getClassloader() { AccessController.doPrivileged((PrivilegedAction) () -> { return new URLClassLoader(contextName, urls, this.getClass().getClassLoader()); }); + LocalCachingContextCleaner.registerClassLoader(cl); classloader.set(cl); LOG.trace("New classloader created from URLs: {}", Arrays.asList(classloader.get().getURLs())); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java index 14e7c32..75d1a23 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.management.ManagementFactory; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; @@ -35,9 +36,17 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import javax.management.InstanceAlreadyExistsException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; + import org.apache.accumulo.classloader.lcc.cache.CacheUtils; import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.classloader.lcc.definition.Resource; +import org.apache.accumulo.classloader.lcc.jmx.ContextClassLoaders; +import org.apache.accumulo.classloader.lcc.jmx.ContextClassLoadersMXBean; import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; import org.apache.accumulo.core.spi.common.ContextClassLoaderEnvironment; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; @@ -196,6 +205,16 @@ public void init(ContextClassLoaderEnvironment env) { } catch (IOException | ContextClassLoaderException e) { throw new IllegalStateException("Error creating base cache directory at " + baseCacheDir, e); } + try { + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + mbs.registerMBean(new ContextClassLoaders(), ContextClassLoadersMXBean.getObjectName()); + } catch (MalformedObjectNameException | MBeanRegistrationException + | NotCompliantMBeanException e) { + throw new IllegalStateException("Error registering MBean", e); + } catch (InstanceAlreadyExistsException e) { + // instance was re-init'd. This is likely to happen during tests + // can ignore as no issue here + } } @Override diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java new file mode 100644 index 0000000..7f816e3 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java @@ -0,0 +1,55 @@ +/* + * 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 + * + * https://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.accumulo.classloader.lcc; + +import java.io.IOException; +import java.lang.ref.Cleaner; +import java.lang.ref.SoftReference; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LocalCachingContextCleaner { + + private static final Logger LOG = LoggerFactory.getLogger(LocalCachingContextCleaner.class); + private static final List> LOADERS = new ArrayList<>(); + private static final Cleaner CLEANER = Cleaner.create(); + + public static void registerClassLoader(final URLClassLoader cl) { + LOADERS.add(new SoftReference<>(cl)); + CLEANER.register(cl, () -> { + LOADERS.removeIf((sr) -> sr.get() == cl); + try { + cl.close(); + } catch (IOException ioe) { + LOG.warn("Error closing LocalCachingContext URLClassLoader", ioe); + } + }); + } + + public static List getReferencedClassLoaders() { + List cll = new ArrayList<>(); + LOADERS.forEach(sr -> cll.add(sr.get())); + return cll; + } + +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index d1d180c..491b54a 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -25,19 +25,40 @@ import java.io.InputStream; import java.net.URL; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.TreeSet; import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; +import org.apache.accumulo.core.cli.Help; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.accumulo.start.spi.KeywordExecutable; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; +import com.beust.jcommander.Parameter; import com.google.common.base.Preconditions; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @SuppressFBWarnings(value = {"EI_EXPOSE_REP"}) -public class ContextDefinition { +public class ContextDefinition implements KeywordExecutable { + + static class Opts extends Help { + @Parameter(names = {"-n", "--name"}, required = true, description = "context name", arity = 1, + order = 1) + String contextName; + + @Parameter(names = {"-i", "--interval"}, required = true, + description = "monitor interval (in seconds)", arity = 1, order = 2) + int monitorInterval; + + @Parameter(names = {"-f", "--files"}, required = true, description = "-f [ -f ...]", + arity = -1, order = 3) + public List files = new ArrayList<>(); + } public static ContextDefinition create(String contextName, int monitorIntervalSecs, URL... sources) throws ContextClassLoaderException, IOException { @@ -124,4 +145,35 @@ public synchronized byte[] getChecksum() throws NoSuchAlgorithmException { public String toJson() { return Constants.GSON.toJson(this); } + + @Override + public String keyword() { + return "create-context-definition"; + } + + @Override + public String usage() { + return KeywordExecutable.super.usage(); + } + + @Override + public String description() { + return "Creates and prints a Context Definition"; + } + + @Override + public void execute(String[] args) throws Exception { + Configuration hadoopConf = new Configuration(); + URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory(hadoopConf)); + + Opts opts = new Opts(); + opts.parseArgs(ContextDefinition.class.getName(), args); + URL[] urls = new URL[opts.files.size()]; + int count = 0; + for (String f : opts.files) { + urls[count++] = new URL(f); + } + ContextDefinition def = create(opts.contextName, opts.monitorInterval, urls); + System.out.println(def.toJson()); + } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoaders.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoaders.java new file mode 100644 index 0000000..563d831 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoaders.java @@ -0,0 +1,43 @@ +/* + * 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 + * + * https://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.accumulo.classloader.lcc.jmx; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.accumulo.classloader.lcc.LocalCachingContextCleaner; + +public class ContextClassLoaders implements ContextClassLoadersMXBean { + + @Override + public Set getReferencedFiles() { + final List loaders = LocalCachingContextCleaner.getReferencedClassLoaders(); + final Set files = new HashSet<>(); + loaders.forEach(l -> { + for (URL u : l.getURLs()) { + files.add(u.toString()); + } + }); + return files; + } + +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoadersMXBean.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoadersMXBean.java new file mode 100644 index 0000000..4cb4135 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/jmx/ContextClassLoadersMXBean.java @@ -0,0 +1,34 @@ +/* + * 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 + * + * https://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.accumulo.classloader.lcc.jmx; + +import java.util.Set; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +public interface ContextClassLoadersMXBean { + + static ObjectName getObjectName() throws MalformedObjectNameException { + return new ObjectName("org.apache.accumulo.classloader:type=ContextClassLoaders"); + } + + Set getReferencedFiles(); + +} diff --git a/modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable b/modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable new file mode 100644 index 0000000..2ed18f8 --- /dev/null +++ b/modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable @@ -0,0 +1,20 @@ +# +# 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 +# +# https://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. +# + +org.apache.accumulo.classloader.lcc.definition.ContextDefinition \ No newline at end of file diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java index 587de84..0dcd960 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java @@ -224,10 +224,8 @@ public void testInitialInvalidJson() throws Exception { def.toJson().substring(0, 4)); final URL invalidDefUrl = new URL(fs.getUri().toString() + invalid.toUri().toString()); - ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, + assertThrows(ContextClassLoaderException.class, () -> FACTORY.getClassLoader(invalidDefUrl.toString())); - assertTrue(ex.getMessage().startsWith( - "Error getting classloader for context: com.google.gson.stream.MalformedJsonException")); } @Test diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java index 6e6fed2..edb4ae5 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java @@ -40,13 +40,21 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import javax.management.JMX; +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; +import org.apache.accumulo.classloader.lcc.jmx.ContextClassLoadersMXBean; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.AccumuloException; @@ -75,14 +83,23 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.tools.attach.VirtualMachine; +import com.sun.tools.attach.VirtualMachineDescriptor; public class MiniAccumuloClusterClassLoaderFactoryTest extends SharedMiniClusterBase { + private static final Logger LOG = + LoggerFactory.getLogger(MiniAccumuloClusterClassLoaderFactoryTest.class); + private static class TestMACConfiguration implements MiniClusterConfigurationCallback { @Override public void configureMiniCluster(MiniAccumuloConfigImpl cfg, org.apache.hadoop.conf.Configuration coreSite) { + cfg.removeJvmOption("-XX:+PerfDisableSharedMem"); cfg.setNumTservers(3); cfg.setProperty(Property.TSERV_NATIVEMAP_ENABLED.getKey(), "false"); cfg.setProperty(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(), @@ -128,14 +145,16 @@ public static void afterAll() throws Exception { @Test public void testClassLoader() throws Exception { - - Path baseDirPath = tempDir.resolve("base"); - Path jsonDirPath = baseDirPath.resolve("contextFiles"); + final String contextName = "test"; + final Path baseDirPath = tempDir.resolve("base"); + final String contextCacheDirPath = baseDirPath.resolve(contextName).toUri().toURL().toString(); + final Path jsonDirPath = baseDirPath.resolve("contextFiles"); Files.createDirectory(jsonDirPath, PERMISSIONS); // Create a context definition that only references jar A final ContextDefinition testContextDef = - ContextDefinition.create("test", MONITOR_INTERVAL_SECS, jarAOrigLocation); + ContextDefinition.create(contextName, MONITOR_INTERVAL_SECS, jarAOrigLocation); + final String jarAHash = testContextDef.getResources().iterator().next().getChecksum(); final String testContextDefJson = testContextDef.toJson(); final File testContextDefFile = jsonDirPath.resolve("testContextDefinition.json").toFile(); Files.writeString(testContextDefFile.toPath(), testContextDefJson, StandardOpenOption.CREATE); @@ -197,6 +216,10 @@ public void testClassLoader() throws Exception { // before applying the iterator final byte[] jarAValueBytes = "foo".getBytes(UTF_8); assertEquals(0, countExpectedValues(client, tableName, jarAValueBytes)); + Set refFiles = getReferencedFiles(); + assertEquals(1, refFiles.size()); + assertTrue(refFiles + .contains(contextCacheDirPath + File.separator + "example-iterators-a.jar_" + jarAHash)); // Attach a scan iterator to the table IteratorSetting is = new IteratorSetting(101, "example", ITER_CLASS_NAME); @@ -208,10 +231,15 @@ public void testClassLoader() throws Exception { while (count != 1000) { count = countExpectedValues(client, tableName, jarAValueBytes); } + refFiles = getReferencedFiles(); + assertEquals(1, refFiles.size()); + assertTrue(refFiles + .contains(contextCacheDirPath + File.separator + "example-iterators-a.jar_" + jarAHash)); // Update the context definition to point to jar B final ContextDefinition testContextDefUpdate = - ContextDefinition.create("test", MONITOR_INTERVAL_SECS, jarBOrigLocation); + ContextDefinition.create(contextName, MONITOR_INTERVAL_SECS, jarBOrigLocation); + final String jarBHash = testContextDefUpdate.getResources().iterator().next().getChecksum(); final String testContextDefUpdateJson = testContextDefUpdate.toJson(); Files.writeString(testContextDefFile.toPath(), testContextDefUpdateJson, StandardOpenOption.TRUNCATE_EXISTING); @@ -225,6 +253,12 @@ public void testClassLoader() throws Exception { // by the iterator final byte[] jarBValueBytes = "bar".getBytes(UTF_8); assertEquals(1000, countExpectedValues(client, tableName, jarBValueBytes)); + refFiles = getReferencedFiles(); + assertEquals(2, refFiles.size()); + assertTrue(refFiles + .contains(contextCacheDirPath + File.separator + "example-iterators-a.jar_" + jarAHash)); + assertTrue(refFiles + .contains(contextCacheDirPath + File.separator + "example-iterators-b.jar_" + jarBHash)); // Copy jar A, create a context definition using the copy, then // remove the copy so that it's not found when the context classloader @@ -238,7 +272,7 @@ public void testClassLoader() throws Exception { assertTrue(Files.exists(jarACopy)); final ContextDefinition testContextDefUpdate2 = - ContextDefinition.create("test", MONITOR_INTERVAL_SECS, jarACopy.toUri().toURL()); + ContextDefinition.create(contextName, MONITOR_INTERVAL_SECS, jarACopy.toUri().toURL()); Files.delete(jarACopy); assertTrue(!Files.exists(jarACopy)); @@ -254,6 +288,12 @@ public void testClassLoader() throws Exception { // by the iterator. The previous class is still being used after // the monitor interval because the jar referenced does not exist. assertEquals(1000, countExpectedValues(client, tableName, jarBValueBytes)); + refFiles = getReferencedFiles(); + assertEquals(2, refFiles.size()); + assertTrue(refFiles + .contains(contextCacheDirPath + File.separator + "example-iterators-a.jar_" + jarAHash)); + assertTrue(refFiles + .contains(contextCacheDirPath + File.separator + "example-iterators-b.jar_" + jarBHash)); // Wait 2 minutes, 2 times the UPDATE_FAILURE_GRACE_PERIOD_MINS Thread.sleep(120_000); @@ -267,6 +307,37 @@ public void testClassLoader() throws Exception { } } + public Set getReferencedFiles() { + final Set referencedFiles = new HashSet<>(); + List vmdl = VirtualMachine.list(); + for (VirtualMachineDescriptor vmd : vmdl) { + if (vmd.displayName().contains("org.apache.accumulo.start.Main")) { + LOG.info("Attempting to connect to {}", vmd.displayName()); + try { + VirtualMachine vm = VirtualMachine.attach(vmd); + String connectorAddress = vm.getAgentProperties() + .getProperty("com.sun.management.jmxremote.localConnectorAddress"); + if (connectorAddress == null) { + connectorAddress = vm.startLocalManagementAgent(); + connectorAddress = vm.getAgentProperties() + .getProperty("com.sun.management.jmxremote.localConnectorAddress"); + } + JMXServiceURL url = new JMXServiceURL(connectorAddress); + try (JMXConnector connector = JMXConnectorFactory.connect(url)) { + MBeanServerConnection mbsc = connector.getMBeanServerConnection(); + ContextClassLoadersMXBean proxy = JMX.newMXBeanProxy(mbsc, + ContextClassLoadersMXBean.getObjectName(), ContextClassLoadersMXBean.class); + referencedFiles.addAll(proxy.getReferencedFiles()); + } + } catch (Exception e) { + LOG.error("Error getting referenced files from {}", vmd.displayName(), e); + } + } + } + LOG.info("Referenced files: {}", referencedFiles); + return referencedFiles; + } + private int countExpectedValues(AccumuloClient client, String table, byte[] expectedValue) throws TableNotFoundException, AccumuloSecurityException, AccumuloException { Scanner scanner = client.createScanner(table); diff --git a/pom.xml b/pom.xml index 125cb8f..dae98a7 100644 --- a/pom.xml +++ b/pom.xml @@ -135,7 +135,7 @@ under the License. org.apache.accumulo accumulo-project - 2.1.4 + 2.1.5-SNAPSHOT pom import @@ -411,7 +411,7 @@ under the License. - + From a8a059b4afc042be21a340db8df35b21a7b25dbf Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 4 Dec 2025 12:27:05 +0000 Subject: [PATCH 02/15] Add -Dapache.snapshots=true to MAVEN_OPTS in GH workflows --- .github/workflows/maven-on-demand.yaml | 2 +- .github/workflows/maven.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven-on-demand.yaml b/.github/workflows/maven-on-demand.yaml index 5bdd47c..ca60d24 100644 --- a/.github/workflows/maven-on-demand.yaml +++ b/.github/workflows/maven-on-demand.yaml @@ -50,7 +50,7 @@ jobs: timeout-minutes: 345 run: mvn -B -V -e -ntp "-Dstyle.color=always" ${{ github.event.inputs.goals }} env: - MAVEN_OPTS: -Djansi.force=true + MAVEN_OPTS: -Djansi.force=true -Dapache.snapshots=true - name: Upload unit test results if: ${{ failure() }} uses: actions/upload-artifact@v4 diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml index 099e04f..434a20e 100644 --- a/.github/workflows/maven.yaml +++ b/.github/workflows/maven.yaml @@ -45,7 +45,7 @@ jobs: - name: Build with Maven (verify javadoc:jar) run: mvn -B -V -e -ntp "-Dstyle.color=always" verify javadoc:jar -DskipFormat -DverifyFormat env: - MAVEN_OPTS: -Djansi.force=true + MAVEN_OPTS: -Djansi.force=true -Dapache.snapshots=true - name: Upload unit test results if: ${{ failure() }} uses: actions/upload-artifact@v4 From c674f776da02fed0d49177b2fafdf0098f6ea7ec Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 4 Dec 2025 17:51:43 +0000 Subject: [PATCH 03/15] Simplified code in ContextDefinition --- .../classloader/lcc/definition/ContextDefinition.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index 491b54a..7fb34f3 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -151,11 +151,6 @@ public String keyword() { return "create-context-definition"; } - @Override - public String usage() { - return KeywordExecutable.super.usage(); - } - @Override public String description() { return "Creates and prints a Context Definition"; @@ -163,8 +158,7 @@ public String description() { @Override public void execute(String[] args) throws Exception { - Configuration hadoopConf = new Configuration(); - URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory(hadoopConf)); + URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory(new Configuration())); Opts opts = new Opts(); opts.parseArgs(ContextDefinition.class.getName(), args); From 52e6548a9e9e7032f6d3208b79edb70611e317d9 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 14 Jan 2026 23:30:35 +0000 Subject: [PATCH 04/15] Removed cleaner --- .../lcc/LocalCachingContextCleaner.java | 43 ------------------- .../lcc/definition/ContextDefinition.java | 3 -- .../classloader/lcc/util/LccUtils.java | 2 - 3 files changed, 48 deletions(-) delete mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java deleted file mode 100644 index 2814df9..0000000 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextCleaner.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 - * - * https://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.accumulo.classloader.lcc; - -import java.io.IOException; -import java.lang.ref.Cleaner; -import java.net.URLClassLoader; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class LocalCachingContextCleaner { - - private static final Logger LOG = LoggerFactory.getLogger(LocalCachingContextCleaner.class); - private static final Cleaner CLEANER = Cleaner.create(); - - public static void registerClassLoader(final URLClassLoader cl) { - CLEANER.register(cl, () -> { - try { - cl.close(); - } catch (IOException ioe) { - LOG.warn("Error closing LocalCachingContext URLClassLoader", ioe); - } - }); - } - -} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index 085d548..3e926a5 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -48,9 +48,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -@SuppressFBWarnings(value = {"EI_EXPOSE_REP"}) public class ContextDefinition implements KeywordExecutable { static class Opts extends Help { diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LccUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LccUtils.java index 7ea3d33..e9c67f2 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LccUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LccUtils.java @@ -23,7 +23,6 @@ import java.util.Arrays; import org.apache.accumulo.classloader.lcc.LocalCachingContextClassLoaderFactory; -import org.apache.accumulo.classloader.lcc.LocalCachingContextCleaner; import org.apache.commons.codec.digest.DigestUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +42,6 @@ public static URLClassLoader createClassLoader(String name, URL[] urls) { if (LOG.isTraceEnabled()) { LOG.trace("New classloader created for {} from URLs: {}", name, Arrays.asList(urls)); } - LocalCachingContextCleaner.registerClassLoader(cl); return cl; } From 710c104664dd1044fd224455d2cba7b306db6d25 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 14:13:24 +0000 Subject: [PATCH 05/15] Fix Test --- ...AccumuloClusterClassLoaderFactoryTest.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java index af36844..6a71b8a 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java @@ -226,7 +226,8 @@ public void testClassLoader() throws Exception { assertEquals(0, countExpectedValues(client, tableName, jarAValueBytes)); Set refFiles = getReferencedFiles(); assertEquals(1, refFiles.size()); - assertTrue(refFiles.contains(resourcesDirPath + jarALocalFileName)); + assertTrue(refFiles + .contains(resourcesDirPath.resolve(jarALocalFileName).toUri().toURL().toString())); // Attach a scan iterator to the table IteratorSetting is = new IteratorSetting(101, "example", ITER_CLASS_NAME); @@ -240,7 +241,8 @@ public void testClassLoader() throws Exception { } refFiles = getReferencedFiles(); assertEquals(1, refFiles.size()); - assertTrue(refFiles.contains(resourcesDirPath + jarALocalFileName)); + assertTrue(refFiles + .contains(resourcesDirPath.resolve(jarALocalFileName).toUri().toURL().toString())); // Update the context definition to point to jar B final ContextDefinition testContextDefUpdate = @@ -265,8 +267,10 @@ public void testClassLoader() throws Exception { assertEquals(1000, countExpectedValues(client, tableName, jarBValueBytes)); refFiles = getReferencedFiles(); assertEquals(2, refFiles.size()); - assertTrue(refFiles.contains(resourcesDirPath + jarALocalFileName)); - assertTrue(refFiles.contains(resourcesDirPath + jarBLocalFileName)); + assertTrue(refFiles + .contains(resourcesDirPath.resolve(jarALocalFileName).toUri().toURL().toString())); + assertTrue(refFiles + .contains(resourcesDirPath.resolve(jarBLocalFileName).toUri().toURL().toString())); // Copy jar A, create a context definition using the copy, then // remove the copy so that it's not found when the context classloader @@ -298,8 +302,10 @@ public void testClassLoader() throws Exception { assertEquals(1000, countExpectedValues(client, tableName, jarBValueBytes)); refFiles = getReferencedFiles(); assertEquals(2, refFiles.size()); - assertTrue(refFiles.contains(resourcesDirPath + jarALocalFileName)); - assertTrue(refFiles.contains(resourcesDirPath + jarBLocalFileName)); + assertTrue(refFiles + .contains(resourcesDirPath.resolve(jarALocalFileName).toUri().toURL().toString())); + assertTrue(refFiles + .contains(resourcesDirPath.resolve(jarBLocalFileName).toUri().toURL().toString())); // Wait 2 minutes, 2 times the UPDATE_FAILURE_GRACE_PERIOD_MINS Thread.sleep(120_000); @@ -317,7 +323,8 @@ public Set getReferencedFiles() { final Map> referencedFiles = new HashMap<>(); List vmdl = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : vmdl) { - if (vmd.displayName().contains("org.apache.accumulo.start.Main")) { + if (vmd.displayName().contains("org.apache.accumulo.start.Main") + && !vmd.displayName().contains("zookeeper")) { LOG.info("Attempting to connect to {}", vmd.displayName()); try { VirtualMachine vm = VirtualMachine.attach(vmd); @@ -340,9 +347,9 @@ public Set getReferencedFiles() { } } } - LOG.info("Referenced files: {}", referencedFiles); Set justTheFiles = new HashSet<>(); referencedFiles.values().forEach(justTheFiles::addAll); + LOG.info("Referenced files: {}", justTheFiles); return justTheFiles; } From 5cb172ac20cd8013aece07d012118094419e241e Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 14:18:03 +0000 Subject: [PATCH 06/15] Updated parameter order due to context name parameter being removed --- .../classloader/lcc/definition/ContextDefinition.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index bf0e962..723860e 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -52,11 +52,11 @@ public class ContextDefinition implements KeywordExecutable { static class Opts extends Help { @Parameter(names = {"-i", "--interval"}, required = true, - description = "monitor interval (in seconds)", arity = 1, order = 2) + description = "monitor interval (in seconds)", arity = 1, order = 1) int monitorInterval; @Parameter(names = {"-f", "--files"}, required = true, description = "-f [ -f ...]", - arity = -1, order = 3) + arity = -1, order = 2) public List files = new ArrayList<>(); } From 514ba791046f2e5155732367eba43f3b6b8bbd8f Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 09:22:33 -0500 Subject: [PATCH 07/15] Apply suggestion from @keith-turner Co-authored-by: Keith Turner --- modules/local-caching-classloader/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index ba3eb5f..a63427f 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -189,7 +189,7 @@ To aid in this task a JMX MXBean has been created to expose the files that are s by the classloaders that are created. For an example of how to use this MXBean, please see the test method `MiniAccumuloClusterClassLoaderFactoryTest.getReferencedFiles`. This method attaches to the local Accumulo JVM processes to get the set of referenced files. It should be safe to delete files that are located in the base cache directory (set by property `general.custom.classloader.lcc.cache.dir`) that are NOT in the set -of referenced files. +of referenced files and existed before references were gathered. **IMPORTANT**: as mentioned earlier, it is not safe to delete resource files that are still referenced by any `ClassLoader` instances. Each `ClassLoader` From a0057555d1a47ad19e7fc41ef639746cda271f02 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 14:42:52 +0000 Subject: [PATCH 08/15] Remove service file and use AutoService instead --- modules/local-caching-classloader/pom.xml | 29 +++++++++++++++++++ .../lcc/definition/ContextDefinition.java | 2 ++ .../lcc/util/DeduplicationCache.java | 1 + ...pache.accumulo.start.spi.KeywordExecutable | 20 ------------- 4 files changed, 32 insertions(+), 20 deletions(-) delete mode 100644 modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 397c79c..434a96a 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -51,6 +51,15 @@ spotbugs-annotations true + + com.google.auto.service + auto-service + + + com.google.auto.service + auto-service-annotations + 1.1.1 + com.beust jcommander @@ -142,6 +151,26 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + true + true + + -Xlint:all + -Xlint:-processing + -Xmaxwarns + 5 + + + + com.google.auto.service + auto-service + + + + org.apache.maven.plugins maven-dependency-plugin diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index 723860e..c94db50 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -43,11 +43,13 @@ import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; import com.beust.jcommander.Parameter; +import com.google.auto.service.AutoService; import com.google.common.base.Preconditions; import com.google.common.base.Suppliers; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +@AutoService(KeywordExecutable.class) public class ContextDefinition implements KeywordExecutable { static class Opts extends Help { diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java index ee65e13..ececbf5 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java @@ -77,6 +77,7 @@ public boolean anyMatch(final Predicate keyPredicate) { } public void values(final BiConsumer consumer) { + canonicalWeakValuesCache.cleanUp(); canonicalWeakValuesCache.asMap().forEach(consumer); } diff --git a/modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable b/modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable deleted file mode 100644 index 2ed18f8..0000000 --- a/modules/local-caching-classloader/src/main/resources/META-INF/services/org.apache.accumulo.start.spi.KeywordExecutable +++ /dev/null @@ -1,20 +0,0 @@ -# -# 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 -# -# https://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. -# - -org.apache.accumulo.classloader.lcc.definition.ContextDefinition \ No newline at end of file From 122bd6cd7d9f27aadc571ef94230f6d25e818789 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 15:53:53 +0000 Subject: [PATCH 09/15] Removed auto-service dependency --- modules/local-caching-classloader/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 434a96a..72c1bb2 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -51,10 +51,6 @@ spotbugs-annotations true - - com.google.auto.service - auto-service - com.google.auto.service auto-service-annotations From 54ea87bd40f59e46e51b3c79c0c5a373e7c6dc76 Mon Sep 17 00:00:00 2001 From: Christopher Tubbs Date: Thu, 15 Jan 2026 13:18:51 -0500 Subject: [PATCH 10/15] Clean up README and pom --- modules/local-caching-classloader/README.md | 42 +++++++++++---------- pom.xml | 7 +++- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index a63427f..528beb1 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -122,16 +122,18 @@ download, and any modification will likely cause unexpected behavior. ## Creating a ContextDefinition file Users may take advantage of the `ContextDefinition.create(int,URL[])` method to -construct a `ContextDefinition` object. This will calculate the checksums of -the classpath elements. `ContextDefinition.toJson()` can be used to serialize -the `ContextDefinition` to a `String` to store in a file. +construct a `ContextDefinition` object, programmatically. This will calculate +the checksums of the classpath elements. `ContextDefinition.toJson()` can be +used to serialize the `ContextDefinition` to a `String` to store in a file. -Alternatively, if the local-caching-classloader jar is built and placed into the $ACCUMULO_HOME/lib directory, then -the `$ACCUMULO_HOME/bin/accumulo create-context-definition` command can be used to create the -ContextDefinition json. The command takes 3 arguments: the name of the context, the monitor interval, -and a list of file URLs. The resulting json is printed to stdout and can be redirected to a file. Users -may take advantage of the `ContextDefinition.create` and `ContextDefinition.toJson` methods to -construct a ContextDefinition object if they wish to do this programmatically. +Alternatively, if this library's jar is built and placed onto Accumulo's +`CLASSPATH`, then one can run `bin/accumulo create-context-definition` to +create the ContextDefinition json file using the command-line. The resulting +json is printed to stdout and can be redirected to a file. The command takes +two arguments: + +1. the monitor interval, in seconds (e.g. `-i 300`), and +2. a list of file URLs (e.g. `-f hdfs://host:port/path/to/one.jar http://host/path/to/two.jar`) ## Updating a ContextDefinition file @@ -177,7 +179,6 @@ constructed at that time. ## Cleanup - Because the cache directory is shared among multiple processes, and one process can't know what the other processes are doing, this class cannot clean up the shared cache directory of unused resources. It is left to the user to remove @@ -185,19 +186,22 @@ unused files from the cache. While the context definition JSON files are always safe to delete, it is not recommended to do so for any that are still in use, because they can be useful for troubleshooting. -To aid in this task a JMX MXBean has been created to expose the files that are still referenced - by the classloaders that are created. For an example of how to use this MXBean, please see the test method `MiniAccumuloClusterClassLoaderFactoryTest.getReferencedFiles`. This method attaches to the -local Accumulo JVM processes to get the set of referenced files. It should be safe to delete files that are located -in the base cache directory (set by property `general.custom.classloader.lcc.cache.dir`) that are NOT in the set -of referenced files and existed before references were gathered. +To aid in this task, a JMX MXBean has been created to expose the files that are +still referenced by the classloaders that have been created by this factory and +currently still exist in the system. For an example of how to use this MXBean, +please see the test method +`MiniAccumuloClusterClassLoaderFactoryTest.getReferencedFiles`. This method +attaches to the local Accumulo JVM processes to get the set of referenced +files. It should be safe to delete files that are located in the local cache +directory (set by property `general.custom.classloader.lcc.cache.dir`) that are +NOT in the set of referenced files, so long as no new classloaders have been +created that reference the files being deleted. **IMPORTANT**: as mentioned earlier, it is not safe to delete resource files that are still referenced by any `ClassLoader` instances. Each `ClassLoader` instance assumes that the locally cached resources exist and can be read. They -will not attempt to download any files. Downloading and verifying files only -occurs when `ClassLoader` instances are initially created for a context -definition. - +will not attempt to download any files. Downloading files only occurs when +`ClassLoader` instances are initially created for a context definition. ## Accumulo Configuration diff --git a/pom.xml b/pom.xml index f3771ac..f3bed03 100644 --- a/pom.xml +++ b/pom.xml @@ -419,7 +419,12 @@ under the License. - + + + + org.apache.accumulo:*:*-SNAPSHOT + + From 4258435f23e1f7b3958a5e377723e15f1b381fe8 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 19:08:35 +0000 Subject: [PATCH 11/15] Updated create-context-definition example in README --- modules/local-caching-classloader/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index 528beb1..78524d2 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -133,7 +133,7 @@ json is printed to stdout and can be redirected to a file. The command takes two arguments: 1. the monitor interval, in seconds (e.g. `-i 300`), and -2. a list of file URLs (e.g. `-f hdfs://host:port/path/to/one.jar http://host/path/to/two.jar`) +2. a list of file URLs (e.g. `-f hdfs://host:port/path/to/one.jar -f file://host/path/to/two.jar`) ## Updating a ContextDefinition file From 41b65f46b013ef36fdb951c9776499ef9835dd7d Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 19:31:21 +0000 Subject: [PATCH 12/15] Implemented command line argument change suggestion --- modules/local-caching-classloader/README.md | 2 +- .../accumulo/classloader/lcc/definition/ContextDefinition.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index 78524d2..cb5589c 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -133,7 +133,7 @@ json is printed to stdout and can be redirected to a file. The command takes two arguments: 1. the monitor interval, in seconds (e.g. `-i 300`), and -2. a list of file URLs (e.g. `-f hdfs://host:port/path/to/one.jar -f file://host/path/to/two.jar`) +2. a list of file URLs (e.g. `hdfs://host:port/path/to/one.jar file://host/path/to/two.jar`) ## Updating a ContextDefinition file diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index c94db50..bb415bb 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -57,7 +57,7 @@ static class Opts extends Help { description = "monitor interval (in seconds)", arity = 1, order = 1) int monitorInterval; - @Parameter(names = {"-f", "--files"}, required = true, description = "-f [ -f ...]", + @Parameter(required = true, description = "classpath element URL ([ ...])", arity = -1, order = 2) public List files = new ArrayList<>(); } From 03737abad8390ab8045e0047d948010741e16032 Mon Sep 17 00:00:00 2001 From: Christopher Tubbs Date: Thu, 15 Jan 2026 18:17:55 -0500 Subject: [PATCH 13/15] Code review cleanup * POM improvements * Use var and multi-catch in getReferencedFiles * Add newline to toJson() * Check for nulls before calling user consumer * Rename method in DeduplicationCache (values to forEach) --- modules/local-caching-classloader/pom.xml | 25 ++-------------- ...LocalCachingContextClassLoaderFactory.java | 12 ++++---- .../lcc/definition/ContextDefinition.java | 8 +++-- .../lcc/util/DeduplicationCache.java | 8 +++-- ...AccumuloClusterClassLoaderFactoryTest.java | 29 +++++++++---------- pom.xml | 6 ++++ 6 files changed, 40 insertions(+), 48 deletions(-) diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 72c1bb2..0428499 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -52,9 +52,10 @@ true + com.google.auto.service - auto-service-annotations - 1.1.1 + auto-service + true com.beust @@ -147,26 +148,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - true - true - - -Xlint:all - -Xlint:-processing - -Xmaxwarns - 5 - - - - com.google.auto.service - auto-service - - - - org.apache.maven.plugins maven-dependency-plugin diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java index 9134dd2..94fed63 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java @@ -302,14 +302,12 @@ private void checkMonitoredLocation(String contextLocation, long interval) { public static Map> getReferencedFiles() { final Map> referencedContexts = new HashMap<>(); - LocalCachingContextClassLoaderFactory.classloaders.values((defUrl, cl) -> { - if (cl != null) { - List files = new ArrayList<>(); - for (URL u : cl.getURLs()) { - files.add(u.toString()); - } - referencedContexts.put(defUrl, files); + classloaders.forEach((cacheKey, cl) -> { + List files = new ArrayList<>(); + for (URL u : cl.getURLs()) { + files.add(u.toString()); } + referencedContexts.put(cacheKey, files); }); return referencedContexts; } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index bb415bb..8c13fee 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -62,6 +62,7 @@ static class Opts extends Help { public List files = new ArrayList<>(); } + // pretty-print uses Unix newline private static final Gson GSON = new GsonBuilder().disableJdkUnsafe().setPrettyPrinting().create(); @@ -141,7 +142,10 @@ public String getChecksum() { } public String toJson() { - return GSON.toJson(this); + // GSON pretty print uses Unix line-endings, and may or may not have a trailing one, so + // ensure a trailing one exists, so it's included in checksum computations and in + // any files written from this (for better readability) + return GSON.toJson(this).stripTrailing() + "\n"; } @Override @@ -166,6 +170,6 @@ public void execute(String[] args) throws Exception { urls[count++] = new URL(f); } ContextDefinition def = create(opts.monitorInterval, urls); - System.out.println(def.toJson()); + System.out.print(def.toJson()); } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java index 7bb68f0..4edaa46 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCache.java @@ -76,9 +76,13 @@ public boolean anyMatch(final Predicate keyPredicate) { return canonicalWeakValuesCache.asMap().keySet().stream().anyMatch(keyPredicate); } - public void values(final BiConsumer consumer) { + public void forEach(final BiConsumer consumer) { canonicalWeakValuesCache.cleanUp(); - canonicalWeakValuesCache.asMap().forEach(consumer); + canonicalWeakValuesCache.asMap().forEach((k, v) -> { + if (v != null) { + consumer.accept(k, v); + } + }); } } diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java index 6a71b8a..51605ee 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; +import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -52,8 +53,7 @@ import java.util.stream.Collectors; import javax.management.JMX; -import javax.management.MBeanServerConnection; -import javax.management.remote.JMXConnector; +import javax.management.MalformedObjectNameException; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; @@ -93,6 +93,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; @@ -154,7 +155,7 @@ public static void afterAll() throws Exception { public void testClassLoader() throws Exception { final var baseDirPath = tempDir.resolve("base"); final var resourcesDirPath = baseDirPath.resolve("resources"); - final var jsonDirPath = baseDirPath.resolve("contextFiles"); + final var jsonDirPath = tempDir.resolve("simulatedRemoteContextFiles"); Files.createDirectory(jsonDirPath, PERMISSIONS); // Create a context definition that only references jar A @@ -319,36 +320,34 @@ public void testClassLoader() throws Exception { } } - public Set getReferencedFiles() { + private Set getReferencedFiles() { final Map> referencedFiles = new HashMap<>(); - List vmdl = VirtualMachine.list(); - for (VirtualMachineDescriptor vmd : vmdl) { + for (VirtualMachineDescriptor vmd : VirtualMachine.list()) { if (vmd.displayName().contains("org.apache.accumulo.start.Main") && !vmd.displayName().contains("zookeeper")) { LOG.info("Attempting to connect to {}", vmd.displayName()); try { - VirtualMachine vm = VirtualMachine.attach(vmd); + var vm = VirtualMachine.attach(vmd); String connectorAddress = vm.getAgentProperties() .getProperty("com.sun.management.jmxremote.localConnectorAddress"); if (connectorAddress == null) { connectorAddress = vm.startLocalManagementAgent(); - connectorAddress = vm.getAgentProperties() - .getProperty("com.sun.management.jmxremote.localConnectorAddress"); } - JMXServiceURL url = new JMXServiceURL(connectorAddress); - try (JMXConnector connector = JMXConnectorFactory.connect(url)) { - MBeanServerConnection mbsc = connector.getMBeanServerConnection(); - ContextClassLoadersMXBean proxy = JMX.newMXBeanProxy(mbsc, - ContextClassLoadersMXBean.getObjectName(), ContextClassLoadersMXBean.class); + var url = new JMXServiceURL(connectorAddress); + try (var connector = JMXConnectorFactory.connect(url)) { + var mbsc = connector.getMBeanServerConnection(); + var proxy = JMX.newMXBeanProxy(mbsc, ContextClassLoadersMXBean.getObjectName(), + ContextClassLoadersMXBean.class); referencedFiles.putAll(proxy.getReferencedFiles()); } - } catch (Exception e) { + } catch (MalformedObjectNameException | AttachNotSupportedException | IOException e) { LOG.error("Error getting referenced files from {}", vmd.displayName(), e); } } } Set justTheFiles = new HashSet<>(); referencedFiles.values().forEach(justTheFiles::addAll); + LOG.info("Referenced files with contexts: {}", referencedFiles); LOG.info("Referenced files: {}", justTheFiles); return justTheFiles; } diff --git a/pom.xml b/pom.xml index f3bed03..0335e9d 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,13 @@ under the License. true + + + com.google.auto.service:auto-service-annotations:jar:* + + + com.google.auto.service:auto-service:jar:* org.apache.commons:commons-vfs2-hdfs:* org.apache.httpcomponents.client5:httpclient5:* From c0f1978b5895c7649bdc63b8754484115cf6c64b Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 15 Jan 2026 23:50:55 +0000 Subject: [PATCH 14/15] Changes due to apache/accumulo#6062 --- .../lcc/MiniAccumuloClusterClassLoaderFactoryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java index 51605ee..2f7f55b 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java @@ -107,7 +107,7 @@ private static class TestMACConfiguration implements MiniClusterConfigurationCal @Override public void configureMiniCluster(MiniAccumuloConfigImpl cfg, org.apache.hadoop.conf.Configuration coreSite) { - cfg.removeJvmOption("-XX:+PerfDisableSharedMem"); + cfg.getJvmOptions().remove("-XX:+PerfDisableSharedMem"); cfg.setNumTservers(3); cfg.setProperty(Property.TSERV_NATIVEMAP_ENABLED.getKey(), "false"); cfg.setProperty(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(), From 59bb5c70e23c164e7b11b4f06376ce69d2bafad8 Mon Sep 17 00:00:00 2001 From: Christopher Tubbs Date: Thu, 15 Jan 2026 19:35:15 -0500 Subject: [PATCH 15/15] Override mini jvm config for JMX --- .../lcc/MiniAccumuloClusterClassLoaderFactoryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java index 2f7f55b..07909eb 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java @@ -107,7 +107,7 @@ private static class TestMACConfiguration implements MiniClusterConfigurationCal @Override public void configureMiniCluster(MiniAccumuloConfigImpl cfg, org.apache.hadoop.conf.Configuration coreSite) { - cfg.getJvmOptions().remove("-XX:+PerfDisableSharedMem"); + cfg.getJvmOptions().add("-XX:-PerfDisableSharedMem"); cfg.setNumTservers(3); cfg.setProperty(Property.TSERV_NATIVEMAP_ENABLED.getKey(), "false"); cfg.setProperty(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(),