From 59515ab0a2c3164b131546ee51051a24dd309137 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 25 Sep 2025 14:56:36 +0000 Subject: [PATCH 01/31] Initial commit for VFSClassLoader replacement --- modules/local-caching-classloader/.gitignore | 36 +++ modules/local-caching-classloader/pom.xml | 53 ++++ .../accumulo/classloader/lcc/Constants.java | 34 +++ ...LocalCachingContextClassLoaderFactory.java | 152 +++++++++++ .../classloader/lcc/cache/CacheUtils.java | 100 +++++++ .../lcc/manifest/ContextDefinition.java | 59 +++++ .../classloader/lcc/manifest/Manifest.java | 55 ++++ .../classloader/lcc/manifest/Resource.java | 64 +++++ .../lcc/resolvers/FileResolver.java | 61 +++++ .../lcc/resolvers/HdfsFileResolver.java | 62 +++++ .../lcc/resolvers/HttpFileResolver.java | 41 +++ .../lcc/resolvers/LocalFileResolver.java | 64 +++++ .../lcc/state/ContextClassLoader.java | 247 ++++++++++++++++++ .../classloaders/lcc/state/Contexts.java | 106 ++++++++ pom.xml | 3 +- 15 files changed, 1136 insertions(+), 1 deletion(-) create mode 100644 modules/local-caching-classloader/.gitignore create mode 100644 modules/local-caching-classloader/pom.xml create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/Constants.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/ContextClassLoader.java create mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java diff --git a/modules/local-caching-classloader/.gitignore b/modules/local-caching-classloader/.gitignore new file mode 100644 index 0000000..55d7f58 --- /dev/null +++ b/modules/local-caching-classloader/.gitignore @@ -0,0 +1,36 @@ +# +# 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. +# + +# Maven ignores +/target/ + +# IDE ignores +/.settings/ +/.project +/.classpath +/.pydevproject +/.idea +/*.iml +/*.ipr +/*.iws +/nbproject/ +/nbactions.xml +/nb-configuration.xml +/.vscode/ +/.factorypath diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml new file mode 100644 index 0000000..052871c --- /dev/null +++ b/modules/local-caching-classloader/pom.xml @@ -0,0 +1,53 @@ + + + + 4.0.0 + + org.apache.accumulo + classloader-extras + 1.0.0-SNAPSHOT + ../../pom.xml + + local-caching-classloader + + ../../src/build/eclipse-codestyle.xml + + + + commons-codec + commons-codec + provided + + + + org.apache.accumulo + accumulo-core + provided + + + + + + + + + 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 new file mode 100644 index 0000000..49bf7bc --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/Constants.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; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class Constants { + + public static final String CACHE_DIR_PROPERTY = "accumulo.classloader.cache.dir"; + public static final String MANIFEST_URL_PROPERTY = "accumulo.classloader.manifest.url"; + public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(0); + public static final Gson GSON = new GsonBuilder().disableJdkUnsafe().create(); + +} 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 new file mode 100644 index 0000000..6654bf1 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java @@ -0,0 +1,152 @@ +/* + * 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.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.accumulo.classloader.lcc.cache.CacheUtils; +import org.apache.accumulo.classloader.lcc.manifest.Manifest; +import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; +import org.apache.accumulo.classloaders.lcc.state.Contexts; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A ContextClassLoaderFactory implementation that does the following creates and maintains a + * ClassLoader for a named context. This factory expects the system property + * {@code Constants#MANIFEST_URL_PROPERTY} to be set to the URL of a json formatted manifest file. + * The manifest file contains an interval at which this class should monitor the manifest file for + * changes and a mapping of context names to ContextDefinitions. Each ContextDefinition contains a + * monitoring interval and a list of resources. Each resource is defined by a URL to the file and an + * expected MD5 hash value. + * + * The URLs supplied for the manifest file and for the resources can use one of the following + * protocols: file://, http://, or hdfs://. + * + * As this class processes the ContextDefinitions it fetches the contents of the resource from the + * resource URL and caches it in a directory on the local filesystem. This class uses the value of + * thesystem property {@code Constants#CACHE_DIR_PROPERTY} as the root directory and creates a + * subdirectory for each context name. Each context cache directory contains a lock file and a copy + * of each fetched resource that is named using the following format: + * fileName_md5Hash.fileNameSuffix. + * + * The lock file prevents processes from manipulating the contexts of the context cache directory + * concurrently, which enables the cache directories to be shared among multiple processes on the + * host. + * + * Note that 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. + * + */ +public class LocalCachingContextClassLoaderFactory implements ContextClassLoaderFactory { + + private static final Logger LOG = + LoggerFactory.getLogger(LocalCachingContextClassLoaderFactory.class); + + private final AtomicReference manifestLocation = new AtomicReference<>(); + private final AtomicReference manifest = new AtomicReference<>(); + private final Contexts contexts = new Contexts(manifest); + + private Manifest parseManifest(URL url) throws ContextClassLoaderException { + LOG.trace("Retrieving manifest file from {}", url); + FileResolver resolver = FileResolver.resolve(url); + try { + try (InputStream is = resolver.getInputStream()) { + return Constants.GSON.fromJson(new InputStreamReader(is), Manifest.class); + } + } catch (IOException e) { + throw new ContextClassLoaderException("Error reading manifest file: " + resolver.getURL(), e); + } + } + + private URL getAndMonitorManifest() throws ContextClassLoaderException { + final String manifestPropValue = System.getProperty(Constants.MANIFEST_URL_PROPERTY); + if (manifestPropValue == null) { + throw new ContextClassLoaderException( + "System property " + Constants.MANIFEST_URL_PROPERTY + " not set."); + } + try { + final URL url = new URL(manifestPropValue); + final Manifest m = parseManifest(url); + manifest.compareAndSet(null, m); + contexts.update(); + Constants.EXECUTOR.scheduleWithFixedDelay(() -> { + try { + final AtomicBoolean updateRequired = new AtomicBoolean(false); + final Manifest mUpdate = parseManifest(manifestLocation.get()); + manifest.getAndAccumulate(mUpdate, (curr, update) -> { + try { + // If the Manifest file has not changed, then continue to use + // the current Manifest. If it has changed, then update the + // Contexts and use the new one. + if (Arrays.equals(curr.getChecksum(), update.getChecksum())) { + LOG.trace("Manifest file has not changed"); + return curr; + } else { + LOG.debug("Manifest file has changed, updating contexts"); + updateRequired.set(true); + return update; + } + } catch (NoSuchAlgorithmException e) { + LOG.error( + "Error computing checksum during manifest update, retaining current manifest", e); + return curr; + } + }); + if (updateRequired.get()) { + contexts.update(); + } + } catch (Exception e) { + LOG.error("Error parsing manifest at " + url); + } + }, m.getMonitorIntervalSeconds(), m.getMonitorIntervalSeconds(), TimeUnit.SECONDS); + LOG.debug("Monitoring manifest file {} for changes at {} second intervals", url, + m.getMonitorIntervalSeconds()); + return url; + } catch (IOException e) { + throw new ContextClassLoaderException( + "Error parsing manifest at " + Constants.MANIFEST_URL_PROPERTY, e); + } + } + + @Override + public ClassLoader getClassLoader(String contextName) throws ContextClassLoaderException { + + // If the location is not already set, get the Manifest location, + // parse it, and start a thread to monitor it for updates. + if (manifestLocation.get() == null) { + CacheUtils.createBaseCacheDir(); + manifestLocation.compareAndSet(null, getAndMonitorManifest()); + } + + return contexts.getContextClassLoader(contextName); + } + +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java new file mode 100644 index 0000000..33f08a4 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -0,0 +1,100 @@ +/* + * 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.cache; + +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.accumulo.classloader.lcc.Constants; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.accumulo.core.util.Pair; + +public class CacheUtils { + + private static final Set CACHE_DIR_PERMS = + EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, OTHERS_READ); + private static final FileAttribute> PERMISSIONS = + PosixFilePermissions.asFileAttribute(CACHE_DIR_PERMS); + private static final String lockFileName = "lock_file"; + + public static Path mkdir(Path p) throws ContextClassLoaderException { + try { + return Files.createDirectory(p, PERMISSIONS); + } catch (FileAlreadyExistsException e) { + return p; + } catch (IOException e) { + throw new ContextClassLoaderException( + "Error creating cache directory: " + p.toFile().getAbsolutePath(), e); + } + } + + public static Path createBaseCacheDir() throws ContextClassLoaderException { + final String prop = Constants.CACHE_DIR_PROPERTY; + final String cacheDir = System.getProperty(prop); + if (cacheDir == null) { + throw new ContextClassLoaderException("System property " + prop + " not set."); + } + return mkdir(Paths.get(URI.create(cacheDir))); + } + + public static Path createOrGetContextCacheDir(String contextName) + throws ContextClassLoaderException { + Path baseContextDir = createBaseCacheDir(); + return mkdir(baseContextDir.resolve(contextName)); + } + + public static Pair lockContextCacheDir(Path contextCacheDir) + throws ContextClassLoaderException { + Path lockFilePath = contextCacheDir.resolve(lockFileName); + try { + FileChannel channel = FileChannel.open(lockFilePath, + EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE), PERMISSIONS); + FileLock lock = channel.tryLock(); + if (lock == null) { + // something else has the lock + channel.close(); + return null; + } else { + return new Pair<>(channel, lock); + } + } catch (IOException e) { + throw new ContextClassLoaderException("Error creating lock file in context cache directory " + + contextCacheDir.toFile().getAbsolutePath(), e); + } + } + +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java new file mode 100644 index 0000000..4b9c22f --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java @@ -0,0 +1,59 @@ +/* + * 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.manifest; + +import java.util.List; +import java.util.Objects; + +public class ContextDefinition { + private final int contextMonitorIntervalSeconds; + private final List resources; + + public ContextDefinition(int contextMonitorIntervalSeconds, List resources) { + super(); + this.contextMonitorIntervalSeconds = contextMonitorIntervalSeconds; + this.resources = resources; + } + + public int getContextMonitorIntervalSeconds() { + return contextMonitorIntervalSeconds; + } + + public List getResources() { + return resources; + } + + @Override + public int hashCode() { + return Objects.hash(contextMonitorIntervalSeconds, resources); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ContextDefinition other = (ContextDefinition) obj; + return contextMonitorIntervalSeconds == other.contextMonitorIntervalSeconds + && Objects.equals(resources, other.resources); + } +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java new file mode 100644 index 0000000..ca3916e --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.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.manifest; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +import org.apache.accumulo.classloader.lcc.Constants; + +public class Manifest { + private final int monitorIntervalSeconds; + private final Map contexts; + private volatile transient byte[] checksum = null; + + public Manifest(int monitorIntervalSeconds, Map contexts) { + super(); + this.monitorIntervalSeconds = monitorIntervalSeconds; + this.contexts = contexts; + } + + public int getMonitorIntervalSeconds() { + return monitorIntervalSeconds; + } + + public Map getContexts() { + return contexts; + } + + public synchronized byte[] getChecksum() throws NoSuchAlgorithmException { + if (checksum == null) { + checksum = + MessageDigest.getInstance("MD5").digest(Constants.GSON.toJson(this).getBytes(UTF_8)); + } + return checksum; + } +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java new file mode 100644 index 0000000..1297f28 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java @@ -0,0 +1,64 @@ +/* + * 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.manifest; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; + +public class Resource { + + private final String location; + private final String checksum; + + public Resource(String location, String checksum) { + super(); + this.location = location; + this.checksum = checksum; + } + + public URL getURL() throws MalformedURLException { + return new URL(location); + } + + public String getLocation() { + return location; + } + + public String getChecksum() { + return checksum; + } + + @Override + public int hashCode() { + return Objects.hash(checksum, location); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Resource other = (Resource) obj; + return Objects.equals(checksum, other.checksum) && Objects.equals(location, other.location); + } +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java new file mode 100644 index 0000000..c47452d --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java @@ -0,0 +1,61 @@ +/* + * 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.resolvers; + +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; + +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; + +public abstract class FileResolver { + + public static FileResolver resolve(URL url) throws ContextClassLoaderException { + String protocol = url.getProtocol(); + switch (protocol) { + case "http": + case "https": + return new HttpFileResolver(url); + case "file": + return new LocalFileResolver(url); + case "hdfs": + return new HdfsFileResolver(url); + default: + throw new ContextClassLoaderException("Unhandled protocol: " + protocol); + } + } + + protected final URL url; + + protected FileResolver(URL url) throws ContextClassLoaderException { + this.url = url; + } + + public URL getURL() { + return this.url; + } + + public String getFileName() throws URISyntaxException { + return Paths.get(getURL().toURI()).getFileName().toString(); + } + + public abstract InputStream getInputStream() throws ContextClassLoaderException; + +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java new file mode 100644 index 0000000..018b904 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java @@ -0,0 +1,62 @@ +/* + * 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.resolvers; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; + +public class HdfsFileResolver extends FileResolver { + + private final Configuration hadoopConf = new Configuration(); + private final FileSystem fs; + private final Path path; + + protected HdfsFileResolver(URL url) throws ContextClassLoaderException { + super(url); + try { + final URI uri = url.toURI(); + this.fs = FileSystem.get(uri, hadoopConf); + this.path = fs.makeQualified(new Path(uri)); + if (!fs.exists(this.path)) { + throw new ContextClassLoaderException("File: " + url + " does not exist."); + } + } catch (URISyntaxException e) { + throw new ContextClassLoaderException("Error creating URI from url: " + url, e); + } catch (IOException e) { + throw new ContextClassLoaderException("Error resolving file from url: " + url, e); + } + } + + @Override + public InputStream getInputStream() throws ContextClassLoaderException { + try { + return fs.open(path); + } catch (IOException e) { + throw new ContextClassLoaderException("Error opening file at url: " + url, e); + } + } +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java new file mode 100644 index 0000000..6989487 --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java @@ -0,0 +1,41 @@ +/* + * 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.resolvers; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; + +public class HttpFileResolver extends FileResolver { + + protected HttpFileResolver(URL url) throws ContextClassLoaderException { + super(url); + } + + @Override + public InputStream getInputStream() throws ContextClassLoaderException { + try { + return url.openStream(); + } catch (IOException e) { + throw new ContextClassLoaderException("Error opening file at url: " + url, e); + } + } +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java new file mode 100644 index 0000000..2e95a6c --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java @@ -0,0 +1,64 @@ +/* + * 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.resolvers; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.hadoop.shaded.org.apache.commons.io.FileUtils; + +public class LocalFileResolver extends FileResolver { + + private final File file; + + public LocalFileResolver(URL url) throws ContextClassLoaderException { + super(url); + if (url.getHost() != null) { + throw new ContextClassLoaderException( + "Unsupported file url, only local files are supported. url = " + url); + } + try { + final URI uri = url.toURI(); + final Path path = Paths.get(uri); + if (Files.notExists(Paths.get(uri))) { + throw new ContextClassLoaderException("File: " + url + " does not exist."); + } + file = path.toFile(); + } catch (URISyntaxException e) { + throw new ContextClassLoaderException("Error creating URI from url: " + url, e); + } + } + + @Override + public FileInputStream getInputStream() throws ContextClassLoaderException { + try { + return FileUtils.openInputStream(file); + } catch (IOException e) { + throw new ContextClassLoaderException("Error opening file at url: " + url, e); + } + } +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/ContextClassLoader.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/ContextClassLoader.java new file mode 100644 index 0000000..418954a --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/ContextClassLoader.java @@ -0,0 +1,247 @@ +/* + * 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.classloaders.lcc.state; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.accumulo.classloader.lcc.Constants; +import org.apache.accumulo.classloader.lcc.cache.CacheUtils; +import org.apache.accumulo.classloader.lcc.manifest.ContextDefinition; +import org.apache.accumulo.classloader.lcc.manifest.Manifest; +import org.apache.accumulo.classloader.lcc.manifest.Resource; +import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.accumulo.core.util.Pair; +import org.apache.commons.codec.digest.DigestUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ContextClassLoader { + + public static class ClassPathElement { + private final FileResolver remote; + private final URL localCachedCopy; + private final String localCachedCopyDigest; + + public ClassPathElement(FileResolver remote, URL localCachedCopy, + String localCachedCopyDigest) { + super(); + this.remote = remote; + this.localCachedCopy = localCachedCopy; + this.localCachedCopyDigest = localCachedCopyDigest; + } + + public FileResolver getRemote() { + return remote; + } + + public URL getLocalCachedCopy() { + return localCachedCopy; + } + + public String getLocalCachedCopyDigest() { + return localCachedCopyDigest; + } + } + + private ClassPathElement cacheResource(final Resource resource) throws Exception { + + final DigestUtils digest = new DigestUtils("MD5"); + final FileResolver source = FileResolver.resolve(resource.getURL()); + final Path cacheLocation = + contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum()); + final File cacheFile = cacheLocation.toFile(); + if (!Files.exists(cacheLocation)) { + Files.copy(source.getInputStream(), cacheLocation); + String md5 = digest.digestAsHex(cacheFile); + if (!resource.getChecksum().equals(digest)) { + // What we just wrote does not match the Manifest. + } + return new ClassPathElement(source, cacheFile.toURI().toURL(), md5); + } else { + // File exists, return new ClassPathElement based on existing file + String fileName = cacheFile.getName(); + String[] parts = fileName.split("_"); + return new ClassPathElement(source, cacheFile.toURI().toURL(), parts[1]); + } + + } + + private static final Logger LOG = LoggerFactory.getLogger(ContextClassLoader.class); + + private final AtomicReference manifest; + private final Path contextCacheDir; + private final String contextName; + private final Set elements = new HashSet<>(); + private final AtomicBoolean elementsChanged = new AtomicBoolean(true); + private volatile ContextDefinition definition; + private volatile WeakReference classloader = null; + private volatile ScheduledFuture refreshTask; + + public ContextClassLoader(AtomicReference manifest, String name) + throws ContextClassLoaderException { + this.manifest = manifest; + this.contextName = name; + this.definition = manifest.get().getContexts().get(contextName); + this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(contextName); + } + + public ContextDefinition getDefinition() { + return definition; + } + + private void scheduleRefresh() { + // Schedule a one-shot task in the event the monitor interval changes + this.refreshTask = Constants.EXECUTOR.schedule(() -> update(), + this.definition.getContextMonitorIntervalSeconds(), TimeUnit.SECONDS); + } + + private void update() { + ContextDefinition update = manifest.get().getContexts().get(contextName); + if (update != null) { + updateDefinition(update); + } + } + + public void initialize() { + try { + synchronized (elements) { + final Pair lockPair = CacheUtils.lockContextCacheDir(contextCacheDir); + if (lockPair == null) { + // something else is updating this directory + return; + } + try { + for (Resource r : definition.getResources()) { + ClassPathElement cpe = cacheResource(r); + addElement(cpe); + } + scheduleRefresh(); + } finally { + lockPair.getSecond().release(); + lockPair.getFirst().close(); + } + } + } catch (Exception e) { + LOG.error("Error initializing context: " + contextName, e); + } + } + + private void updateDefinition(ContextDefinition update) { + synchronized (elements) { + try { + final Pair lockPair = CacheUtils.lockContextCacheDir(contextCacheDir); + if (lockPair == null) { + // something else is updating this directory + return; + } + try { + if (!definition.getResources().equals(update.getResources())) { + + for (Resource updatedResource : update.getResources()) { + + ClassPathElement existing = findElementBySourceLocation(updatedResource.getURL()); + if (existing == null) { + // new resource + ClassPathElement cpe = cacheResource(updatedResource); + addElement(cpe); + } else if (existing.getLocalCachedCopyDigest() + .equals(updatedResource.getChecksum())) { + removeElement(existing); + ClassPathElement cpe = cacheResource(updatedResource); + addElement(cpe); + } + } + } + this.definition = update; + scheduleRefresh(); + } finally { + lockPair.getSecond().release(); + lockPair.getFirst().close(); + } + } catch (Exception e) { + LOG.error("Error updating context: " + contextName, e); + } + } + } + + private ClassPathElement findElementBySourceLocation(URL source) { + for (ClassPathElement cpe : elements) { + if (cpe.getRemote().getURL().equals(source)) { + return cpe; + } + } + return null; + } + + private void removeElement(ClassPathElement element) { + synchronized (elements) { + elements.remove(element); + elementsChanged.set(true); + } + } + + private void addElement(ClassPathElement element) { + synchronized (elements) { + elements.add(element); + elementsChanged.set(true); + } + } + + public void clear() { + synchronized (elements) { + refreshTask.cancel(true); + elements.clear(); + elementsChanged.set(false); + if (classloader != null) { + classloader.clear(); + } + } + } + + public ClassLoader getClassloader() { + synchronized (elements) { + if (classloader == null || elementsChanged.get()) { + URL[] urls = new URL[elements.size()]; + Iterator iter = elements.iterator(); + for (int x = 0; x < elements.size(); x++) { + urls[x] = iter.next().getLocalCachedCopy(); + } + elementsChanged.set(false); + classloader = new WeakReference<>( + new URLClassLoader(contextName, urls, this.getClass().getClassLoader())); + } + } + return classloader.get(); + } +} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java new file mode 100644 index 0000000..4fd807e --- /dev/null +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java @@ -0,0 +1,106 @@ +/* + * 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.classloaders.lcc.state; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.accumulo.classloader.lcc.Constants; +import org.apache.accumulo.classloader.lcc.manifest.ContextDefinition; +import org.apache.accumulo.classloader.lcc.manifest.Manifest; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Contexts { + + private static final Logger LOG = LoggerFactory.getLogger(Contexts.class); + + private final AtomicReference manifest; + private final Map contexts = new HashMap<>(); + private final AtomicBoolean updating = new AtomicBoolean(true); + + public Contexts(AtomicReference manifest) { + this.manifest = manifest; + } + + public synchronized void update() { + LOG.debug("Updating all contexts using Manifest"); + updating.set(true); + final Map ctx = manifest.get().getContexts(); + final List removals = new ArrayList<>(); + contexts.keySet().forEach(k -> { + if (!ctx.containsKey(k)) { + removals.add(k); + } + }); + + removals.forEach(r -> { + LOG.debug("Context {} is no longer contained in the Manifest, removing", r); + contexts.get(r).clear(); + contexts.remove(r); + }); + + final List> futures = new ArrayList<>(); + for (Entry e : ctx.entrySet()) { + if (contexts.get(e.getKey()) == null) { + // This is a newly defined context + LOG.debug("Context {} is new in the Manifest, creating new ContextClassLoader", e.getKey()); + try { + ContextClassLoader ccl = new ContextClassLoader(manifest, e.getKey()); + contexts.put(e.getKey(), ccl); + futures.add(Constants.EXECUTOR.submit(() -> ccl.initialize())); + } catch (ContextClassLoaderException e1) { + LOG.error("Error creating new ContextClassLoader for context: " + e.getKey(), e1); + } + } + } + + while (!futures.isEmpty()) { + Iterator> iter = futures.iterator(); + while (iter.hasNext()) { + Future f = iter.next(); + if (f.isDone()) { + iter.remove(); + try { + f.get(); + } catch (InterruptedException | ExecutionException | CancellationException ex) { + LOG.warn("Updating ContextClassLoader with ContextDefinition change failed.", ex); + } + } + } + } + + updating.set(false); + } + + public ClassLoader getContextClassLoader(String context) { + return contexts.get(context).getClassloader(); + } + +} diff --git a/pom.xml b/pom.xml index f2f76d3..0c555a0 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,7 @@ + modules/local-caching-classloader modules/example-iterators-a modules/example-iterators-b modules/vfs-class-loader @@ -128,7 +129,7 @@ under the License.]]> org.apache.accumulo accumulo-project - 2.1.3 + 2.1.4 pom import From afb6c2e810d799f35f91f7a79fc45541ac5c6f8b Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 25 Sep 2025 16:51:57 -0400 Subject: [PATCH 02/31] Update modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java Co-authored-by: Daniel Roberts --- .../classloader/lcc/LocalCachingContextClassLoaderFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6654bf1..ff820bf 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 @@ -124,7 +124,7 @@ private URL getAndMonitorManifest() throws ContextClassLoaderException { contexts.update(); } } catch (Exception e) { - LOG.error("Error parsing manifest at " + url); + LOG.error("Error parsing manifest at {}", url); } }, m.getMonitorIntervalSeconds(), m.getMonitorIntervalSeconds(), TimeUnit.SECONDS); LOG.debug("Monitoring manifest file {} for changes at {} second intervals", url, From e973046cdbcb0cab291f5d8bafc9dae4c784cb26 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 25 Sep 2025 16:52:08 -0400 Subject: [PATCH 03/31] Update modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java Co-authored-by: Daniel Roberts --- .../org/apache/accumulo/classloaders/lcc/state/Contexts.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java index 4fd807e..cfa17ae 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java @@ -76,7 +76,7 @@ public synchronized void update() { contexts.put(e.getKey(), ccl); futures.add(Constants.EXECUTOR.submit(() -> ccl.initialize())); } catch (ContextClassLoaderException e1) { - LOG.error("Error creating new ContextClassLoader for context: " + e.getKey(), e1); + LOG.error("Error creating new ContextClassLoader for context: {}", e.getKey(), e1); } } } From d6f17508d4b4fe0e9678fe796f3755f100e60d30 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 26 Sep 2025 12:16:45 +0000 Subject: [PATCH 04/31] Minor updates --- .../LocalCachingContextClassLoaderFactory.java | 15 +++++++-------- .../lcc/manifest/ContextDefinition.java | 1 - .../classloader/lcc/manifest/Manifest.java | 1 - .../classloader/lcc/manifest/Resource.java | 1 - .../lcc/state/ContextClassLoader.java | 7 +++---- .../lcc/state/Contexts.java | 3 ++- 6 files changed, 12 insertions(+), 16 deletions(-) rename modules/local-caching-classloader/src/main/java/org/apache/accumulo/{classloaders => classloader}/lcc/state/ContextClassLoader.java (97%) rename modules/local-caching-classloader/src/main/java/org/apache/accumulo/{classloaders => classloader}/lcc/state/Contexts.java (97%) 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 6654bf1..d6c2b14 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 @@ -31,19 +31,18 @@ import org.apache.accumulo.classloader.lcc.cache.CacheUtils; import org.apache.accumulo.classloader.lcc.manifest.Manifest; import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; -import org.apache.accumulo.classloaders.lcc.state.Contexts; +import org.apache.accumulo.classloader.lcc.state.Contexts; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A ContextClassLoaderFactory implementation that does the following creates and maintains a - * ClassLoader for a named context. This factory expects the system property - * {@code Constants#MANIFEST_URL_PROPERTY} to be set to the URL of a json formatted manifest file. - * The manifest file contains an interval at which this class should monitor the manifest file for - * changes and a mapping of context names to ContextDefinitions. Each ContextDefinition contains a - * monitoring interval and a list of resources. Each resource is defined by a URL to the file and an - * expected MD5 hash value. + * A ContextClassLoaderFactory implementation that does the creates and maintains a ClassLoader for + * a named context. This factory expects the system property {@code Constants#MANIFEST_URL_PROPERTY} + * to be set to the URL of a json formatted manifest file. The manifest file contains an interval at + * which this class should monitor the manifest file for changes and a mapping of context names to + * ContextDefinitions. Each ContextDefinition contains a monitoring interval and a list of + * resources. Each resource is defined by a URL to the file and an expected MD5 hash value. * * The URLs supplied for the manifest file and for the resources can use one of the following * protocols: file://, http://, or hdfs://. diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java index 4b9c22f..980b297 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java @@ -26,7 +26,6 @@ public class ContextDefinition { private final List resources; public ContextDefinition(int contextMonitorIntervalSeconds, List resources) { - super(); this.contextMonitorIntervalSeconds = contextMonitorIntervalSeconds; this.resources = resources; } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java index ca3916e..29ef492 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java @@ -32,7 +32,6 @@ public class Manifest { private volatile transient byte[] checksum = null; public Manifest(int monitorIntervalSeconds, Map contexts) { - super(); this.monitorIntervalSeconds = monitorIntervalSeconds; this.contexts = contexts; } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java index 1297f28..23b7bf4 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java @@ -28,7 +28,6 @@ public class Resource { private final String checksum; public Resource(String location, String checksum) { - super(); this.location = location; this.checksum = checksum; } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/ContextClassLoader.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java similarity index 97% rename from modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/ContextClassLoader.java rename to modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java index 418954a..0aac62a 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/ContextClassLoader.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.accumulo.classloaders.lcc.state; +package org.apache.accumulo.classloader.lcc.state; import java.io.File; import java.lang.ref.WeakReference; @@ -55,7 +55,6 @@ public static class ClassPathElement { public ClassPathElement(FileResolver remote, URL localCachedCopy, String localCachedCopyDigest) { - super(); this.remote = remote; this.localCachedCopy = localCachedCopy; this.localCachedCopyDigest = localCachedCopyDigest; @@ -84,8 +83,8 @@ private ClassPathElement cacheResource(final Resource resource) throws Exception if (!Files.exists(cacheLocation)) { Files.copy(source.getInputStream(), cacheLocation); String md5 = digest.digestAsHex(cacheFile); - if (!resource.getChecksum().equals(digest)) { - // What we just wrote does not match the Manifest. + if (!resource.getChecksum().equals(md5)) { + // TODO: What we just wrote does not match the Manifest. } return new ClassPathElement(source, cacheFile.toURI().toURL(), md5); } else { diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java similarity index 97% rename from modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java rename to modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java index 4fd807e..49caef4 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloaders/lcc/state/Contexts.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.accumulo.classloaders.lcc.state; +package org.apache.accumulo.classloader.lcc.state; import java.util.ArrayList; import java.util.HashMap; @@ -100,6 +100,7 @@ public synchronized void update() { } public ClassLoader getContextClassLoader(String context) { + // TODO: Wait while updating? return contexts.get(context).getClassloader(); } From ed03145c24ed91c509fb2718353afab058110e42 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 26 Sep 2025 18:54:04 +0000 Subject: [PATCH 05/31] Removed independent monitoring of contexts, should only change when manifest changes --- ...LocalCachingContextClassLoaderFactory.java | 4 ++-- .../lcc/manifest/ContextDefinition.java | 13 +++------- .../lcc/state/ContextClassLoader.java | 24 ++++--------------- .../classloader/lcc/state/Contexts.java | 13 ++++++---- 4 files changed, 18 insertions(+), 36 deletions(-) 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 3100f00..9125e5c 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 @@ -41,8 +41,8 @@ * a named context. This factory expects the system property {@code Constants#MANIFEST_URL_PROPERTY} * to be set to the URL of a json formatted manifest file. The manifest file contains an interval at * which this class should monitor the manifest file for changes and a mapping of context names to - * ContextDefinitions. Each ContextDefinition contains a monitoring interval and a list of - * resources. Each resource is defined by a URL to the file and an expected MD5 hash value. + * ContextDefinitions. Each ContextDefinition contains a list of resources. Each resource is defined + * by a URL to the file and an expected MD5 hash value. * * The URLs supplied for the manifest file and for the resources can use one of the following * protocols: file://, http://, or hdfs://. diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java index 980b297..1047879 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java @@ -22,25 +22,19 @@ import java.util.Objects; public class ContextDefinition { - private final int contextMonitorIntervalSeconds; private final List resources; - public ContextDefinition(int contextMonitorIntervalSeconds, List resources) { - this.contextMonitorIntervalSeconds = contextMonitorIntervalSeconds; + public ContextDefinition(List resources) { this.resources = resources; } - public int getContextMonitorIntervalSeconds() { - return contextMonitorIntervalSeconds; - } - public List getResources() { return resources; } @Override public int hashCode() { - return Objects.hash(contextMonitorIntervalSeconds, resources); + return Objects.hash(resources); } @Override @@ -52,7 +46,6 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; ContextDefinition other = (ContextDefinition) obj; - return contextMonitorIntervalSeconds == other.contextMonitorIntervalSeconds - && Objects.equals(resources, other.resources); + return Objects.equals(resources, other.resources); } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java index 0aac62a..8309200 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java @@ -30,11 +30,9 @@ import java.util.Iterator; import java.util.Set; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.classloader.lcc.cache.CacheUtils; import org.apache.accumulo.classloader.lcc.manifest.ContextDefinition; import org.apache.accumulo.classloader.lcc.manifest.Manifest; @@ -98,7 +96,6 @@ private ClassPathElement cacheResource(final Resource resource) throws Exception private static final Logger LOG = LoggerFactory.getLogger(ContextClassLoader.class); - private final AtomicReference manifest; private final Path contextCacheDir; private final String contextName; private final Set elements = new HashSet<>(); @@ -109,7 +106,6 @@ private ClassPathElement cacheResource(final Resource resource) throws Exception public ContextClassLoader(AtomicReference manifest, String name) throws ContextClassLoaderException { - this.manifest = manifest; this.contextName = name; this.definition = manifest.get().getContexts().get(contextName); this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(contextName); @@ -119,19 +115,6 @@ public ContextDefinition getDefinition() { return definition; } - private void scheduleRefresh() { - // Schedule a one-shot task in the event the monitor interval changes - this.refreshTask = Constants.EXECUTOR.schedule(() -> update(), - this.definition.getContextMonitorIntervalSeconds(), TimeUnit.SECONDS); - } - - private void update() { - ContextDefinition update = manifest.get().getContexts().get(contextName); - if (update != null) { - updateDefinition(update); - } - } - public void initialize() { try { synchronized (elements) { @@ -145,7 +128,6 @@ public void initialize() { ClassPathElement cpe = cacheResource(r); addElement(cpe); } - scheduleRefresh(); } finally { lockPair.getSecond().release(); lockPair.getFirst().close(); @@ -156,7 +138,10 @@ public void initialize() { } } - private void updateDefinition(ContextDefinition update) { + public void update(ContextDefinition update) { + if (!definition.getResources().equals(update.getResources())) { + return; + } synchronized (elements) { try { final Pair lockPair = CacheUtils.lockContextCacheDir(contextCacheDir); @@ -183,7 +168,6 @@ private void updateDefinition(ContextDefinition update) { } } this.definition = update; - scheduleRefresh(); } finally { lockPair.getSecond().release(); lockPair.getFirst().close(); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java index a881d03..c2b65ff 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java @@ -68,16 +68,21 @@ public synchronized void update() { final List> futures = new ArrayList<>(); for (Entry e : ctx.entrySet()) { - if (contexts.get(e.getKey()) == null) { + ContextClassLoader ccl = contexts.get(e.getKey()); + if (ccl == null) { // This is a newly defined context LOG.debug("Context {} is new in the Manifest, creating new ContextClassLoader", e.getKey()); try { - ContextClassLoader ccl = new ContextClassLoader(manifest, e.getKey()); - contexts.put(e.getKey(), ccl); - futures.add(Constants.EXECUTOR.submit(() -> ccl.initialize())); + ContextClassLoader newCcl = new ContextClassLoader(manifest, e.getKey()); + contexts.put(e.getKey(), newCcl); + futures.add(Constants.EXECUTOR.submit(() -> newCcl.initialize())); } catch (ContextClassLoaderException e1) { LOG.error("Error creating new ContextClassLoader for context: {}", e.getKey(), e1); } + } else { + // Need to update an existing context + LOG.debug("Evaluating context {} to see if it needs to be updated", e.getKey()); + futures.add(Constants.EXECUTOR.submit(() -> ccl.update(e.getValue()))); } } From a13e28db56f0562ef8281688e4cc98f72c366548 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 14 Nov 2025 19:54:40 +0000 Subject: [PATCH 06/31] Updates, new tests, still wip --- modules/local-caching-classloader/pom.xml | 65 ++++++ .../accumulo/classloader/lcc/Constants.java | 7 +- ...va => LocalCachingContextClassLoader.java} | 137 +++++------ ...LocalCachingContextClassLoaderFactory.java | 154 ++++++------ .../classloader/lcc/cache/CacheUtils.java | 46 +++- .../ContextDefinition.java | 35 ++- .../{manifest => definition}/Resource.java | 2 +- .../classloader/lcc/manifest/Manifest.java | 54 ----- .../lcc/resolvers/FileResolver.java | 5 +- .../lcc/resolvers/HdfsFileResolver.java | 5 + .../lcc/resolvers/HttpFileResolver.java | 7 + .../lcc/resolvers/LocalFileResolver.java | 9 +- .../classloader/lcc/state/Contexts.java | 112 --------- .../LocalCachingContextClassLoaderTest.java | 221 ++++++++++++++++++ .../accumulo/classloader/lcc/TestUtils.java | 105 +++++++++ .../classloader/lcc/cache/CacheUtilsTest.java | 143 ++++++++++++ .../lcc/resolvers/FileResolversTest.java | 128 ++++++++++ .../src/test/java/test/HelloWorldTemplate | 27 +++ .../src/test/java/test/Test.java | 27 +++ .../src/test/java/test/TestTemplate | 36 +++ .../src/test/resources/log4j2-test.properties | 35 +++ .../src/test/shell/makeHelloWorldJars.sh | 35 +++ .../src/test/shell/makeTestJars.sh | 33 +++ 23 files changed, 1086 insertions(+), 342 deletions(-) rename modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/{state/ContextClassLoader.java => LocalCachingContextClassLoader.java} (54%) rename modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/{manifest => definition}/ContextDefinition.java (54%) rename modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/{manifest => definition}/Resource.java (96%) delete mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java delete mode 100644 modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java create mode 100644 modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java create mode 100644 modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java create mode 100644 modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java create mode 100644 modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java create mode 100644 modules/local-caching-classloader/src/test/java/test/HelloWorldTemplate create mode 100644 modules/local-caching-classloader/src/test/java/test/Test.java create mode 100644 modules/local-caching-classloader/src/test/java/test/TestTemplate create mode 100644 modules/local-caching-classloader/src/test/resources/log4j2-test.properties create mode 100755 modules/local-caching-classloader/src/test/shell/makeHelloWorldJars.sh create mode 100755 modules/local-caching-classloader/src/test/shell/makeTestJars.sh diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 052871c..3b7ce81 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -43,6 +43,41 @@ accumulo-core provided + + org.apache.hadoop + hadoop-client-minicluster + test + + + org.apache.logging.log4j + log4j-slf4j2-impl + test + + + org.eclipse.jetty + jetty-io + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-servlet + test + + + org.eclipse.jetty + jetty-util + test + + + org.junit.jupiter + junit-jupiter-api + test + @@ -50,4 +85,34 @@ + + + + org.codehaus.mojo + exec-maven-plugin + + + build-test-jars + + exec + + process-test-classes + + src/test/shell/makeTestJars.sh + + + + build-helloworld-jars + + exec + + process-test-classes + + src/test/shell/makeHelloWorldJars.sh + + + + + + 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 49bf7bc..f73f243 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 @@ -21,14 +21,19 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import org.apache.commons.codec.digest.DigestUtils; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; public class Constants { public static final String CACHE_DIR_PROPERTY = "accumulo.classloader.cache.dir"; - public static final String MANIFEST_URL_PROPERTY = "accumulo.classloader.manifest.url"; public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(0); public static final Gson GSON = new GsonBuilder().disableJdkUnsafe().create(); + public static DigestUtils getChecksummer() { + return new DigestUtils("MD5"); + } + } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java similarity index 54% rename from modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java rename to modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java index 8309200..4808abe 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/ContextClassLoader.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java @@ -16,54 +16,52 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.accumulo.classloader.lcc.state; +package org.apache.accumulo.classloader.lcc; import java.io.File; -import java.lang.ref.WeakReference; +import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; import java.util.Iterator; +import java.util.Objects; import java.util.Set; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.apache.accumulo.classloader.lcc.cache.CacheUtils; -import org.apache.accumulo.classloader.lcc.manifest.ContextDefinition; -import org.apache.accumulo.classloader.lcc.manifest.Manifest; -import org.apache.accumulo.classloader.lcc.manifest.Resource; +import org.apache.accumulo.classloader.lcc.cache.CacheUtils.LockInfo; +import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; +import org.apache.accumulo.classloader.lcc.definition.Resource; import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import org.apache.accumulo.core.util.Pair; -import org.apache.commons.codec.digest.DigestUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ContextClassLoader { +public class LocalCachingContextClassLoader { public static class ClassPathElement { - private final FileResolver remote; - private final URL localCachedCopy; + private final FileResolver resolver; + private final URL localCachedCopyLocation; private final String localCachedCopyDigest; - public ClassPathElement(FileResolver remote, URL localCachedCopy, + public ClassPathElement(FileResolver resolver, URL localCachedCopy, String localCachedCopyDigest) { - this.remote = remote; - this.localCachedCopy = localCachedCopy; - this.localCachedCopyDigest = localCachedCopyDigest; + this.resolver = Objects.requireNonNull(resolver, "resolver must be supplied"); + this.localCachedCopyLocation = + Objects.requireNonNull(localCachedCopy, "local cached copy location must be supplied"); + this.localCachedCopyDigest = + Objects.requireNonNull(localCachedCopyDigest, "local cached copy md5 must be supplied"); } - public FileResolver getRemote() { - return remote; + public FileResolver getResolver() { + return resolver; } - public URL getLocalCachedCopy() { - return localCachedCopy; + public URL getLocalCachedCopyLocation() { + return localCachedCopyLocation; } public String getLocalCachedCopyDigest() { @@ -73,64 +71,62 @@ public String getLocalCachedCopyDigest() { private ClassPathElement cacheResource(final Resource resource) throws Exception { - final DigestUtils digest = new DigestUtils("MD5"); final FileResolver source = FileResolver.resolve(resource.getURL()); final Path cacheLocation = contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum()); final File cacheFile = cacheLocation.toFile(); if (!Files.exists(cacheLocation)) { - Files.copy(source.getInputStream(), cacheLocation); - String md5 = digest.digestAsHex(cacheFile); - if (!resource.getChecksum().equals(md5)) { - // TODO: What we just wrote does not match the Manifest. + try (InputStream is = source.getInputStream()) { + Files.copy(is, cacheLocation); } - return new ClassPathElement(source, cacheFile.toURI().toURL(), md5); + final String checksum = Constants.getChecksummer().digestAsHex(cacheFile); + if (!resource.getChecksum().equals(checksum)) { + // TODO: What we just wrote does not match the MD5 in the Resource description. + } + return new ClassPathElement(source, cacheFile.toURI().toURL(), checksum); } else { // File exists, return new ClassPathElement based on existing file String fileName = cacheFile.getName(); String[] parts = fileName.split("_"); return new ClassPathElement(source, cacheFile.toURI().toURL(), parts[1]); } - } - private static final Logger LOG = LoggerFactory.getLogger(ContextClassLoader.class); + private static final Logger LOG = LoggerFactory.getLogger(LocalCachingContextClassLoader.class); private final Path contextCacheDir; private final String contextName; private final Set elements = new HashSet<>(); private final AtomicBoolean elementsChanged = new AtomicBoolean(true); - private volatile ContextDefinition definition; - private volatile WeakReference classloader = null; - private volatile ScheduledFuture refreshTask; + private final AtomicReference classloader = new AtomicReference<>(); + private final AtomicReference definition = new AtomicReference<>(); - public ContextClassLoader(AtomicReference manifest, String name) + public LocalCachingContextClassLoader(ContextDefinition contextDefinition) throws ContextClassLoaderException { - this.contextName = name; - this.definition = manifest.get().getContexts().get(contextName); + this.definition.set(Objects.requireNonNull(contextDefinition, "definition must be supplied")); + this.contextName = this.definition.get().getContextName(); this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(contextName); } public ContextDefinition getDefinition() { - return definition; + return definition.get(); } public void initialize() { try { synchronized (elements) { - final Pair lockPair = CacheUtils.lockContextCacheDir(contextCacheDir); - if (lockPair == null) { + final LockInfo lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); + if (lockInfo == null) { // something else is updating this directory return; } try { - for (Resource r : definition.getResources()) { + for (Resource r : definition.get().getResources()) { ClassPathElement cpe = cacheResource(r); addElement(cpe); } } finally { - lockPair.getSecond().release(); - lockPair.getFirst().close(); + lockInfo.unlock(); } } } catch (Exception e) { @@ -138,39 +134,34 @@ public void initialize() { } } - public void update(ContextDefinition update) { - if (!definition.getResources().equals(update.getResources())) { + public void update(final ContextDefinition update) { + Objects.requireNonNull(update, "definition must be supplied"); + if (definition.get().getResources().equals(update.getResources())) { return; } synchronized (elements) { try { - final Pair lockPair = CacheUtils.lockContextCacheDir(contextCacheDir); - if (lockPair == null) { + final LockInfo lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); + if (lockInfo == null) { // something else is updating this directory return; } try { - if (!definition.getResources().equals(update.getResources())) { - - for (Resource updatedResource : update.getResources()) { - - ClassPathElement existing = findElementBySourceLocation(updatedResource.getURL()); - if (existing == null) { - // new resource - ClassPathElement cpe = cacheResource(updatedResource); - addElement(cpe); - } else if (existing.getLocalCachedCopyDigest() - .equals(updatedResource.getChecksum())) { - removeElement(existing); - ClassPathElement cpe = cacheResource(updatedResource); - addElement(cpe); - } + for (Resource updatedResource : update.getResources()) { + ClassPathElement existing = findElementBySourceLocation(updatedResource.getURL()); + if (existing == null) { + // new resource + ClassPathElement cpe = cacheResource(updatedResource); + addElement(cpe); + } else if (existing.getLocalCachedCopyDigest().equals(updatedResource.getChecksum())) { + removeElement(existing); + ClassPathElement cpe = cacheResource(updatedResource); + addElement(cpe); } } - this.definition = update; + this.definition.set(update); } finally { - lockPair.getSecond().release(); - lockPair.getFirst().close(); + lockInfo.unlock(); } } catch (Exception e) { LOG.error("Error updating context: " + contextName, e); @@ -180,7 +171,7 @@ public void update(ContextDefinition update) { private ClassPathElement findElementBySourceLocation(URL source) { for (ClassPathElement cpe : elements) { - if (cpe.getRemote().getURL().equals(source)) { + if (cpe.getResolver().getURL().equals(source)) { return cpe; } } @@ -201,28 +192,16 @@ private void addElement(ClassPathElement element) { } } - public void clear() { - synchronized (elements) { - refreshTask.cancel(true); - elements.clear(); - elementsChanged.set(false); - if (classloader != null) { - classloader.clear(); - } - } - } - public ClassLoader getClassloader() { synchronized (elements) { - if (classloader == null || elementsChanged.get()) { + if (classloader.get() == null || elementsChanged.get()) { URL[] urls = new URL[elements.size()]; Iterator iter = elements.iterator(); for (int x = 0; x < elements.size(); x++) { - urls[x] = iter.next().getLocalCachedCopy(); + urls[x] = iter.next().getLocalCachedCopyLocation(); } elementsChanged.set(false); - classloader = new WeakReference<>( - new URLClassLoader(contextName, urls, this.getClass().getClassLoader())); + classloader.set(new URLClassLoader(contextName, urls, this.getClass().getClassLoader())); } } return classloader.get(); 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 9125e5c..ff20b98 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 @@ -21,36 +21,34 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.ref.SoftReference; +import java.net.MalformedURLException; import java.net.URL; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import org.apache.accumulo.classloader.lcc.cache.CacheUtils; -import org.apache.accumulo.classloader.lcc.manifest.Manifest; +import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; -import org.apache.accumulo.classloader.lcc.state.Contexts; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A ContextClassLoaderFactory implementation that does the creates and maintains a ClassLoader for - * a named context. This factory expects the system property {@code Constants#MANIFEST_URL_PROPERTY} - * to be set to the URL of a json formatted manifest file. The manifest file contains an interval at - * which this class should monitor the manifest file for changes and a mapping of context names to - * ContextDefinitions. Each ContextDefinition contains a list of resources. Each resource is defined - * by a URL to the file and an expected MD5 hash value. + * A ContextClassLoaderFactory implementation that creates and maintains a ClassLoader for a named + * context. This factory expects the parameter passed to {@code {@link #getClassLoader(String)} to + * be the URL of a json formatted {@link #ContextDefinition} file. The file contains an interval at + * which this class should monitor the file for changes and a list of {@link Resource} objects. Each + * resource is defined by a URL to the file and an expected MD5 hash value. * - * The URLs supplied for the manifest file and for the resources can use one of the following - * protocols: file://, http://, or hdfs://. + * The URLs supplied for the context definition file and for the resources can use one of the + * following protocols: file://, http://, or hdfs://. * - * As this class processes the ContextDefinitions it fetches the contents of the resource from the + * As this class processes the ContextDefinition it fetches the contents of the resource from the * resource URL and caches it in a directory on the local filesystem. This class uses the value of - * thesystem property {@code Constants#CACHE_DIR_PROPERTY} as the root directory and creates a - * subdirectory for each context name. Each context cache directory contains a lock file and a copy + * the system property {@code Constants#CACHE_DIR_PROPERTY} as the root directory and creates a + * sub-directory for each context name. Each context cache directory contains a lock file and a copy * of each fetched resource that is named using the following format: * fileName_md5Hash.fileNameSuffix. * @@ -69,83 +67,83 @@ public class LocalCachingContextClassLoaderFactory implements ContextClassLoader private static final Logger LOG = LoggerFactory.getLogger(LocalCachingContextClassLoaderFactory.class); - private final AtomicReference manifestLocation = new AtomicReference<>(); - private final AtomicReference manifest = new AtomicReference<>(); - private final Contexts contexts = new Contexts(manifest); + private final ConcurrentHashMap> contexts = + new ConcurrentHashMap<>(); - private Manifest parseManifest(URL url) throws ContextClassLoaderException { - LOG.trace("Retrieving manifest file from {}", url); + private ContextDefinition parseContextDefinition(URL url) throws ContextClassLoaderException { + LOG.trace("Retrieving context definition file from {}", url); FileResolver resolver = FileResolver.resolve(url); try { try (InputStream is = resolver.getInputStream()) { - return Constants.GSON.fromJson(new InputStreamReader(is), Manifest.class); + return Constants.GSON.fromJson(new InputStreamReader(is), ContextDefinition.class); } } catch (IOException e) { - throw new ContextClassLoaderException("Error reading manifest file: " + resolver.getURL(), e); + throw new ContextClassLoaderException( + "Error reading context definition file: " + resolver.getURL(), e); } } - private URL getAndMonitorManifest() throws ContextClassLoaderException { - final String manifestPropValue = System.getProperty(Constants.MANIFEST_URL_PROPERTY); - if (manifestPropValue == null) { - throw new ContextClassLoaderException( - "System property " + Constants.MANIFEST_URL_PROPERTY + " not set."); + private void monitorContext(final String contextLocation) { + final SoftReference ccl = contexts.get(contextLocation); + if (ccl == null) { + // context has been removed from the map, no need to check for update + return; } - try { - final URL url = new URL(manifestPropValue); - final Manifest m = parseManifest(url); - manifest.compareAndSet(null, m); - contexts.update(); - Constants.EXECUTOR.scheduleWithFixedDelay(() -> { - try { - final AtomicBoolean updateRequired = new AtomicBoolean(false); - final Manifest mUpdate = parseManifest(manifestLocation.get()); - manifest.getAndAccumulate(mUpdate, (curr, update) -> { - try { - // If the Manifest file has not changed, then continue to use - // the current Manifest. If it has changed, then update the - // Contexts and use the new one. - if (Arrays.equals(curr.getChecksum(), update.getChecksum())) { - LOG.trace("Manifest file has not changed"); - return curr; - } else { - LOG.debug("Manifest file has changed, updating contexts"); - updateRequired.set(true); - return update; - } - } catch (NoSuchAlgorithmException e) { - LOG.error( - "Error computing checksum during manifest update, retaining current manifest", e); - return curr; - } - }); - if (updateRequired.get()) { - contexts.update(); - } - } catch (Exception e) { - LOG.error("Error parsing manifest at {}", url); - } - }, m.getMonitorIntervalSeconds(), m.getMonitorIntervalSeconds(), TimeUnit.SECONDS); - LOG.debug("Monitoring manifest file {} for changes at {} second intervals", url, - m.getMonitorIntervalSeconds()); - return url; - } catch (IOException e) { - throw new ContextClassLoaderException( - "Error parsing manifest at " + Constants.MANIFEST_URL_PROPERTY, e); + final LocalCachingContextClassLoader classLoader = ccl.get(); + if (classLoader == null) { + // classloader has been garbage collected. Remove from the map and return + contexts.remove(contextLocation); + return; } + final ContextDefinition currentDef = classLoader.getDefinition(); + Constants.EXECUTOR.schedule(() -> { + try { + URL contextManifest = new URL(contextLocation); + final ContextDefinition update = parseContextDefinition(contextManifest); + if (!Arrays.equals(currentDef.getChecksum(), update.getChecksum())) { + LOG.debug("Context defintion for {} has changed", currentDef.getContextName()); + classLoader.update(update); + } else { + LOG.debug("Context defintion for {} has not changed", currentDef.getContextName()); + } + monitorContext(contextLocation); + } catch (Exception e) { + LOG.error("Error parsing context definition at {}", contextLocation); + } + }, currentDef.getMonitorIntervalSeconds(), TimeUnit.SECONDS); + LOG.debug("Monitoring context definition file {} for changes at {} second intervals", + contextLocation, currentDef.getMonitorIntervalSeconds()); } @Override - public ClassLoader getClassLoader(String contextName) throws ContextClassLoaderException { - - // If the location is not already set, get the Manifest location, - // parse it, and start a thread to monitor it for updates. - if (manifestLocation.get() == null) { - CacheUtils.createBaseCacheDir(); - manifestLocation.compareAndSet(null, getAndMonitorManifest()); + public ClassLoader getClassLoader(final String contextLocation) + throws ContextClassLoaderException { + try { + SoftReference ccl = + contexts.computeIfAbsent(contextLocation, cn -> { + try { + URL contextManifest = new URL(contextLocation); + CacheUtils.createBaseCacheDir(); + ContextDefinition m = parseContextDefinition(contextManifest); + LocalCachingContextClassLoader newCcl = new LocalCachingContextClassLoader(m); + newCcl.initialize(); + monitorContext(contextLocation); + return new SoftReference<>(newCcl); + } catch (MalformedURLException e) { + throw new RuntimeException("Expected valid URL to context definition file", e); + } catch (ContextClassLoaderException e) { + throw new RuntimeException("Error processing context definition", e); + } + }); + return ccl.get().getClassloader(); + } catch (RuntimeException re) { + Throwable t = re.getCause(); + if (t != null && t instanceof ContextClassLoaderException) { + throw (ContextClassLoaderException) t; + } else { + throw new ContextClassLoaderException(re.getMessage(), re); + } } - - return contexts.getContextClassLoader(contextName); } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index 33f08a4..93377af 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -28,6 +28,7 @@ import java.net.URI; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; @@ -37,11 +38,11 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; +import java.util.Objects; import java.util.Set; import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import org.apache.accumulo.core.util.Pair; public class CacheUtils { @@ -51,7 +52,32 @@ public class CacheUtils { PosixFilePermissions.asFileAttribute(CACHE_DIR_PERMS); private static final String lockFileName = "lock_file"; - public static Path mkdir(Path p) throws ContextClassLoaderException { + public static class LockInfo { + + private final FileChannel channel; + private final FileLock lock; + + public LockInfo(FileChannel channel, FileLock lock) { + this.channel = Objects.requireNonNull(channel, "channel must be supplied"); + this.lock = Objects.requireNonNull(lock, "lock must be supplied"); + } + + public FileChannel getChannel() { + return channel; + } + + public FileLock getLock() { + return lock; + } + + public void unlock() throws IOException { + lock.release(); + channel.close(); + } + + } + + private static Path mkdir(Path p) throws ContextClassLoaderException { try { return Files.createDirectory(p, PERMISSIONS); } catch (FileAlreadyExistsException e) { @@ -77,19 +103,25 @@ public static Path createOrGetContextCacheDir(String contextName) return mkdir(baseContextDir.resolve(contextName)); } - public static Pair lockContextCacheDir(Path contextCacheDir) + public static LockInfo lockContextCacheDir(Path contextCacheDir) throws ContextClassLoaderException { Path lockFilePath = contextCacheDir.resolve(lockFileName); try { FileChannel channel = FileChannel.open(lockFilePath, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE), PERMISSIONS); - FileLock lock = channel.tryLock(); - if (lock == null) { + try { + FileLock lock = channel.tryLock(); + if (lock == null) { + // something else has the lock + channel.close(); + return null; + } else { + return new LockInfo(channel, lock); + } + } catch (OverlappingFileLockException e) { // something else has the lock channel.close(); return null; - } else { - return new Pair<>(channel, lock); } } catch (IOException e) { throw new ContextClassLoaderException("Error creating lock file in context cache directory " diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java similarity index 54% rename from modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java rename to modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java index 1047879..f30a4dc 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/ContextDefinition.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java @@ -16,25 +16,42 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.accumulo.classloader.lcc.manifest; +package org.apache.accumulo.classloader.lcc.definition; +import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Objects; +import org.apache.accumulo.classloader.lcc.Constants; + public class ContextDefinition { + private final String contextName; + private final int monitorIntervalSeconds; private final List resources; + private volatile transient byte[] checksum = null; - public ContextDefinition(List resources) { + public ContextDefinition(String contextName, int monitorIntervalSeconds, + List resources) { + this.contextName = contextName; + this.monitorIntervalSeconds = monitorIntervalSeconds; this.resources = resources; } + public String getContextName() { + return contextName; + } + + public int getMonitorIntervalSeconds() { + return monitorIntervalSeconds; + } + public List getResources() { return resources; } @Override public int hashCode() { - return Objects.hash(resources); + return Objects.hash(contextName, monitorIntervalSeconds, resources); } @Override @@ -46,6 +63,16 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; ContextDefinition other = (ContextDefinition) obj; - return Objects.equals(resources, other.resources); + return Objects.equals(contextName, other.contextName) + && monitorIntervalSeconds == other.monitorIntervalSeconds + && Objects.equals(resources, other.resources); + } + + public synchronized byte[] getChecksum() throws NoSuchAlgorithmException { + if (checksum == null) { + checksum = Constants.getChecksummer().digest(Constants.GSON.toJson(this)); + } + return checksum; } + } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java similarity index 96% rename from modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java rename to modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java index 23b7bf4..a1e891e 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Resource.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.accumulo.classloader.lcc.manifest; +package org.apache.accumulo.classloader.lcc.definition; import java.net.MalformedURLException; import java.net.URL; diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java deleted file mode 100644 index 29ef492..0000000 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/manifest/Manifest.java +++ /dev/null @@ -1,54 +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.manifest; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Map; - -import org.apache.accumulo.classloader.lcc.Constants; - -public class Manifest { - private final int monitorIntervalSeconds; - private final Map contexts; - private volatile transient byte[] checksum = null; - - public Manifest(int monitorIntervalSeconds, Map contexts) { - this.monitorIntervalSeconds = monitorIntervalSeconds; - this.contexts = contexts; - } - - public int getMonitorIntervalSeconds() { - return monitorIntervalSeconds; - } - - public Map getContexts() { - return contexts; - } - - public synchronized byte[] getChecksum() throws NoSuchAlgorithmException { - if (checksum == null) { - checksum = - MessageDigest.getInstance("MD5").digest(Constants.GSON.toJson(this).getBytes(UTF_8)); - } - return checksum; - } -} diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java index c47452d..6466a68 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java @@ -21,7 +21,6 @@ import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; -import java.nio.file.Paths; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; @@ -52,9 +51,7 @@ public URL getURL() { return this.url; } - public String getFileName() throws URISyntaxException { - return Paths.get(getURL().toURI()).getFileName().toString(); - } + public abstract String getFileName() throws URISyntaxException; public abstract InputStream getInputStream() throws ContextClassLoaderException; diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java index 018b904..9f9b193 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java @@ -51,6 +51,11 @@ protected HdfsFileResolver(URL url) throws ContextClassLoaderException { } } + @Override + public String getFileName() throws URISyntaxException { + return this.path.getName(); + } + @Override public InputStream getInputStream() throws ContextClassLoaderException { try { diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java index 6989487..1b2ce2e 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URISyntaxException; import java.net.URL; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; @@ -30,6 +31,12 @@ protected HttpFileResolver(URL url) throws ContextClassLoaderException { super(url); } + @Override + public String getFileName() throws URISyntaxException { + String path = this.url.getPath(); + return path.substring(path.lastIndexOf("/") + 1); + } + @Override public InputStream getInputStream() throws ContextClassLoaderException { try { diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java index 2e95a6c..8abfa64 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java @@ -37,9 +37,9 @@ public class LocalFileResolver extends FileResolver { public LocalFileResolver(URL url) throws ContextClassLoaderException { super(url); - if (url.getHost() != null) { + if (url.getHost() != null && !url.getHost().isBlank()) { throw new ContextClassLoaderException( - "Unsupported file url, only local files are supported. url = " + url); + "Unsupported file url, only local files are supported. host = " + url.getHost()); } try { final URI uri = url.toURI(); @@ -53,6 +53,11 @@ public LocalFileResolver(URL url) throws ContextClassLoaderException { } } + @Override + public String getFileName() throws URISyntaxException { + return Paths.get(getURL().toURI()).getFileName().toString(); + } + @Override public FileInputStream getInputStream() throws ContextClassLoaderException { try { diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java deleted file mode 100644 index c2b65ff..0000000 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/state/Contexts.java +++ /dev/null @@ -1,112 +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.state; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import org.apache.accumulo.classloader.lcc.Constants; -import org.apache.accumulo.classloader.lcc.manifest.ContextDefinition; -import org.apache.accumulo.classloader.lcc.manifest.Manifest; -import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Contexts { - - private static final Logger LOG = LoggerFactory.getLogger(Contexts.class); - - private final AtomicReference manifest; - private final Map contexts = new HashMap<>(); - private final AtomicBoolean updating = new AtomicBoolean(true); - - public Contexts(AtomicReference manifest) { - this.manifest = manifest; - } - - public synchronized void update() { - LOG.debug("Updating all contexts using Manifest"); - updating.set(true); - final Map ctx = manifest.get().getContexts(); - final List removals = new ArrayList<>(); - contexts.keySet().forEach(k -> { - if (!ctx.containsKey(k)) { - removals.add(k); - } - }); - - removals.forEach(r -> { - LOG.debug("Context {} is no longer contained in the Manifest, removing", r); - contexts.get(r).clear(); - contexts.remove(r); - }); - - final List> futures = new ArrayList<>(); - for (Entry e : ctx.entrySet()) { - ContextClassLoader ccl = contexts.get(e.getKey()); - if (ccl == null) { - // This is a newly defined context - LOG.debug("Context {} is new in the Manifest, creating new ContextClassLoader", e.getKey()); - try { - ContextClassLoader newCcl = new ContextClassLoader(manifest, e.getKey()); - contexts.put(e.getKey(), newCcl); - futures.add(Constants.EXECUTOR.submit(() -> newCcl.initialize())); - } catch (ContextClassLoaderException e1) { - LOG.error("Error creating new ContextClassLoader for context: {}", e.getKey(), e1); - } - } else { - // Need to update an existing context - LOG.debug("Evaluating context {} to see if it needs to be updated", e.getKey()); - futures.add(Constants.EXECUTOR.submit(() -> ccl.update(e.getValue()))); - } - } - - while (!futures.isEmpty()) { - Iterator> iter = futures.iterator(); - while (iter.hasNext()) { - Future f = iter.next(); - if (f.isDone()) { - iter.remove(); - try { - f.get(); - } catch (InterruptedException | ExecutionException | CancellationException ex) { - LOG.warn("Updating ContextClassLoader with ContextDefinition change failed.", ex); - } - } - } - } - - updating.set(false); - } - - public ClassLoader getContextClassLoader(String context) { - // TODO: Wait while updating? - return contexts.get(context).getClassloader(); - } - -} diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java new file mode 100644 index 0000000..60da08c --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java @@ -0,0 +1,221 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; +import org.apache.accumulo.classloader.lcc.definition.Resource; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hdfs.MiniDFSCluster; +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class LocalCachingContextClassLoaderTest { + + private static final String CONTEXT_NAME = "TEST_CONTEXT"; + private static final int MONITOR_INTERVAL_SECS = 5; + private static MiniDFSCluster hdfs; + private static Server jetty; + private static ContextDefinition def; + + @TempDir + private static java.nio.file.Path tempDir; + + @BeforeAll + public static void beforeAll() throws Exception { + String tmp = tempDir.resolve("base").toUri().toString(); + System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); + + // Find the Test jar files + final URL jarAOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestA/TestA.jar"); + assertNotNull(jarAOrigLocation); + final URL jarBOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestB/TestB.jar"); + assertNotNull(jarBOrigLocation); + final URL jarCOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestC/TestC.jar"); + assertNotNull(jarCOrigLocation); + + // Put B into HDFS + hdfs = TestUtils.getMiniCluster(); + final FileSystem fs = hdfs.getFileSystem(); + assertTrue(fs.mkdirs(new Path("/contextB"))); + final Path dst = new Path("/contextB/TestB.jar"); + fs.copyFromLocalFile(new Path(jarBOrigLocation.toURI()), dst); + assertTrue(fs.exists(dst)); + URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory(hdfs.getConfiguration(0))); + final URL jarBNewLocation = new URL(fs.getUri().toString() + dst.toUri().toString()); + + // Put C into Jetty + java.nio.file.Path jarCParentDirectory = Paths.get(jarCOrigLocation.toURI()).getParent(); + jetty = TestUtils.getJetty(jarCParentDirectory); + final URL jarCNewLocation = jetty.getURI().resolve("TestC.jar").toURL(); + + // Create ContextDefinition with all three resources + final List resources = new ArrayList<>(); + resources.add(new Resource(jarAOrigLocation.toString(), + TestUtils.computeResourceChecksum(jarAOrigLocation))); + resources.add(new Resource(jarBNewLocation.toString(), + TestUtils.computeResourceChecksum(jarBOrigLocation))); + resources.add(new Resource(jarCNewLocation.toString(), + TestUtils.computeResourceChecksum(jarCOrigLocation))); + + def = new ContextDefinition(CONTEXT_NAME, MONITOR_INTERVAL_SECS, resources); + } + + @AfterAll + public static void afterAll() throws Exception { + jetty.stop(); + jetty.join(); + hdfs.shutdown(); + } + + @Test + public void testInitialize() throws ContextClassLoaderException, IOException { + LocalCachingContextClassLoader lcccl = new LocalCachingContextClassLoader(def); + lcccl.initialize(); + + // Confirm the 3 jars are cached locally + final java.nio.file.Path base = Paths.get(tempDir.resolve("base").toUri()); + assertTrue(Files.exists(base)); + assertTrue(Files.exists(base.resolve(CONTEXT_NAME))); + for (Resource r : def.getResources()) { + String filename = TestUtils.getFileName(r.getURL()); + String checksum = r.getChecksum(); + assertTrue(Files.exists(base.resolve(CONTEXT_NAME).resolve(filename + "_" + checksum))); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testClassLoader() throws Exception { + + LocalCachingContextClassLoader lcccl = new LocalCachingContextClassLoader(def); + lcccl.initialize(); + ClassLoader contextClassLoader = lcccl.getClassloader(); + + Class clazzA = + (Class) contextClassLoader.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + + Class clazzB = + (Class) contextClassLoader.loadClass("test.TestObjectB"); + test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); + assertEquals("Hello from B", b1.hello()); + + Class clazzC = + (Class) contextClassLoader.loadClass("test.TestObjectC"); + test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); + assertEquals("Hello from C", c1.hello()); + } + + @SuppressWarnings("unchecked") + @Test + public void testUpdate() throws Exception { + + LocalCachingContextClassLoader lcccl = new LocalCachingContextClassLoader(def); + lcccl.initialize(); + + ClassLoader contextClassLoader = lcccl.getClassloader(); + + Class clazzA = + (Class) contextClassLoader.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + + Class clazzB = + (Class) contextClassLoader.loadClass("test.TestObjectB"); + test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); + assertEquals("Hello from B", b1.hello()); + + Class clazzC = + (Class) contextClassLoader.loadClass("test.TestObjectC"); + test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); + assertEquals("Hello from C", c1.hello()); + + List updatedResources = new ArrayList<>(def.getResources()); + assertEquals(3, updatedResources.size()); + updatedResources.remove(2); // remove C + + // Add D + final URL jarDOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestD/TestD.jar"); + assertNotNull(jarDOrigLocation); + updatedResources.add(new Resource(jarDOrigLocation.toString(), + TestUtils.computeResourceChecksum(jarDOrigLocation))); + + ContextDefinition updatedDef = + new ContextDefinition(CONTEXT_NAME, MONITOR_INTERVAL_SECS, updatedResources); + lcccl.update(updatedDef); + + // Confirm the 3 jars are cached locally + final java.nio.file.Path base = Paths.get(tempDir.resolve("base").toUri()); + assertTrue(Files.exists(base)); + assertTrue(Files.exists(base.resolve(CONTEXT_NAME))); + for (Resource r : updatedDef.getResources()) { + String filename = TestUtils.getFileName(r.getURL()); + assertFalse(filename.contains("C")); + String checksum = r.getChecksum(); + assertTrue(Files.exists(base.resolve(CONTEXT_NAME).resolve(filename + "_" + checksum))); + } + + contextClassLoader = lcccl.getClassloader(); + + clazzA = (Class) contextClassLoader.loadClass("test.TestObjectA"); + test.Test a2 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a2.hello()); + + clazzB = (Class) contextClassLoader.loadClass("test.TestObjectB"); + test.Test b2 = clazzB.getDeclaredConstructor().newInstance(); + assertEquals("Hello from B", b2.hello()); + + // Class C already loaded in the Virtual Machine so this will work even though + // removed from context. When the Garbage Collector unloads this class then + // TestObjectC will not work anymore. + clazzC = (Class) contextClassLoader.loadClass("test.TestObjectC"); + test.Test c2 = clazzC.getDeclaredConstructor().newInstance(); + assertEquals("Hello from C", c2.hello()); + + Class clazzD = + (Class) contextClassLoader.loadClass("test.TestObjectD"); + test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); + assertEquals("Hello from D", d1.hello()); + + } + +} diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java new file mode 100644 index 0000000..2992661 --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java @@ -0,0 +1,105 @@ +/* + * 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.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hdfs.DFSConfigKeys; +import org.apache.hadoop.hdfs.MiniDFSCluster; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.util.resource.PathResource; + +public class TestUtils { + + private static String computeDatanodeDirectoryPermission() { + // MiniDFSCluster will check the permissions on the data directories, but does not + // do a good job of setting them properly. We need to get the users umask and set + // the appropriate Hadoop property so that the data directories will be created + // with the correct permissions. + try { + Process p = Runtime.getRuntime().exec("/bin/sh -c umask"); + try (BufferedReader bri = + new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { + String line = bri.readLine(); + p.waitFor(); + + if (line == null) { + throw new IOException("umask input stream closed prematurely"); + } + short umask = Short.parseShort(line.trim(), 8); + // Need to set permission to 777 xor umask + // leading zero makes java interpret as base 8 + int newPermission = 0777 ^ umask; + + return String.format("%03o", newPermission); + } + } catch (Exception e) { + throw new RuntimeException("Error getting umask from O/S", e); + } + } + + public static MiniDFSCluster getMiniCluster() throws IOException { + System.setProperty("java.io.tmpdir", System.getProperty("user.dir") + "/target"); + + // Put the MiniDFSCluster directory in the target directory + System.setProperty("test.build.data", "target/build/test/data"); + + // Setup HDFS + Configuration conf = new Configuration(); + conf.set("hadoop.security.token.service.use_ip", "true"); + + conf.set("dfs.datanode.data.dir.perm", computeDatanodeDirectoryPermission()); + conf.setLong(DFSConfigKeys.DFS_BLOCK_SIZE_KEY, 1024 * 1024); // 1M blocksize + + MiniDFSCluster cluster = new MiniDFSCluster.Builder(conf).build(); + cluster.waitClusterUp(); + return cluster; + } + + public static Server getJetty(Path resourceDirectory) throws Exception { + PathResource directory = new PathResource(resourceDirectory); + ResourceHandler handler = new ResourceHandler(); + handler.setBaseResource(directory); + + Server jetty = new Server(0); + jetty.setHandler(handler); + jetty.start(); + return jetty; + } + + public static String computeResourceChecksum(URL resourceLocation) throws IOException { + try (InputStream is = resourceLocation.openStream()) { + return Constants.getChecksummer().digestAsHex(is); + } + } + + public static String getFileName(URL url) { + String path = url.getPath(); + return path.substring(path.lastIndexOf("/") + 1); + + } +} diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java new file mode 100644 index 0000000..ac928fc --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java @@ -0,0 +1,143 @@ +/* + * 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.cache; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.accumulo.classloader.lcc.Constants; +import org.apache.accumulo.classloader.lcc.cache.CacheUtils.LockInfo; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +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; + +public class CacheUtilsTest { + + private static final Logger LOG = LoggerFactory.getLogger(CacheUtilsTest.class); + + @TempDir + private static Path tempDir; + + @BeforeAll + public static void beforeAll() { + String tmp = tempDir.resolve("base").toUri().toString(); + LOG.info("Setting cache base directory to {}", tmp); + System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); + } + + @Test + public void testCreateBaseDir() throws Exception { + final Path base = Paths.get(tempDir.resolve("base").toUri()); + try { + assertFalse(Files.exists(base)); + CacheUtils.createBaseCacheDir(); + assertTrue(Files.exists(base)); + } finally { + Files.delete(base); + } + } + + @Test + public void testCreateBaseDirMultipleTimes() throws Exception { + final Path base = Paths.get(tempDir.resolve("base").toUri()); + try { + assertFalse(Files.exists(base)); + CacheUtils.createBaseCacheDir(); + CacheUtils.createBaseCacheDir(); + CacheUtils.createBaseCacheDir(); + CacheUtils.createBaseCacheDir(); + assertTrue(Files.exists(base)); + } finally { + Files.delete(base); + } + } + + @Test + public void createOrGetContextCacheDir() throws Exception { + final Path base = Paths.get(tempDir.resolve("base").toUri()); + try { + assertFalse(Files.exists(base)); + CacheUtils.createOrGetContextCacheDir("context1"); + assertTrue(Files.exists(base)); + assertTrue(Files.exists(base.resolve("context1"))); + CacheUtils.createOrGetContextCacheDir("context2"); + assertTrue(Files.exists(base)); + assertTrue(Files.exists(base.resolve("context2"))); + CacheUtils.createOrGetContextCacheDir("context1"); + assertTrue(Files.exists(base)); + assertTrue(Files.exists(base.resolve("context1"))); + } finally { + Files.delete(base.resolve("context1")); + Files.delete(base.resolve("context2")); + Files.delete(base); + } + } + + @Test + public void testLock() throws Exception { + final Path base = Paths.get(tempDir.resolve("base").toUri()); + final Path cx1 = base.resolve("context1"); + try { + assertFalse(Files.exists(base)); + CacheUtils.createOrGetContextCacheDir("context1"); + assertTrue(Files.exists(base)); + assertTrue(Files.exists(cx1)); + + final LockInfo lockInfo = CacheUtils.lockContextCacheDir(cx1); + try { + assertNotNull(lockInfo); + assertTrue(lockInfo.getLock().acquiredBy().equals(lockInfo.getChannel())); + assertFalse(lockInfo.getLock().isShared()); + assertTrue(lockInfo.getLock().isValid()); + + // Test that another thread can't get the lock + final AtomicReference error = new AtomicReference<>(); + final Thread t = new Thread(() -> { + try { + assertNull(CacheUtils.lockContextCacheDir(cx1)); + } catch (ContextClassLoaderException e) { + error.set(e); + } + }); + t.start(); + t.join(); + assertNull(error.get()); + + } finally { + lockInfo.unlock(); + } + } finally { + Files.delete(cx1.resolve("lock_file")); + Files.delete(cx1); + Files.delete(base); + } + + } + +} diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java new file mode 100644 index 0000000..5bec589 --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java @@ -0,0 +1,128 @@ +/* + * 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.resolvers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.apache.accumulo.classloader.lcc.TestUtils; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.commons.io.IOUtils; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hdfs.MiniDFSCluster; +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FileResolversTest { + + private static final Logger LOG = LoggerFactory.getLogger(FileResolversTest.class); + + private long getFileSize(java.nio.file.Path p) throws IOException { + try (InputStream is = Files.newInputStream(p, StandardOpenOption.READ)) { + return IOUtils.consume(is); + } + } + + private long getFileSize(FileResolver resolver) throws IOException, ContextClassLoaderException { + try (InputStream is = resolver.getInputStream()) { + return IOUtils.consume(is); + } + } + + @Test + public void testLocalFile() throws Exception { + URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar"); + assertNotNull(jarPath); + java.nio.file.Path p = Paths.get(jarPath.toURI()); + final long origFileSize = getFileSize(p); + FileResolver resolver = FileResolver.resolve(jarPath); + assertTrue(resolver instanceof LocalFileResolver); + assertEquals(jarPath, resolver.getURL()); + assertEquals("HelloWorld.jar", resolver.getFileName()); + assertEquals(origFileSize, getFileSize(resolver)); + } + + @Test + public void testHttpFile() throws Exception { + + URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar"); + assertNotNull(jarPath); + java.nio.file.Path p = Paths.get(jarPath.toURI()); + final long origFileSize = getFileSize(p); + + Server jetty = TestUtils.getJetty(p.getParent()); + LOG.debug("Jetty listening at: {}", jetty.getURI()); + URL httpPath = jetty.getURI().resolve("HelloWorld.jar").toURL(); + FileResolver resolver = FileResolver.resolve(httpPath); + assertTrue(resolver instanceof HttpFileResolver); + assertEquals(httpPath, resolver.getURL()); + assertEquals("HelloWorld.jar", resolver.getFileName()); + assertEquals(origFileSize, getFileSize(resolver)); + + jetty.stop(); + jetty.join(); + } + + @Test + public void testHdfsFile() throws Exception { + + URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar"); + assertNotNull(jarPath); + java.nio.file.Path p = Paths.get(jarPath.toURI()); + final long origFileSize = getFileSize(p); + + MiniDFSCluster cluster = TestUtils.getMiniCluster(); + try { + FileSystem fs = cluster.getFileSystem(); + assertTrue(fs.mkdirs(new Path("/context1"))); + Path dst = new Path("/context1/HelloWorld.jar"); + fs.copyFromLocalFile(new Path(jarPath.toURI()), dst); + assertTrue(fs.exists(dst)); + + URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory(cluster.getConfiguration(0))); + + URL fullPath = new URL(fs.getUri().toString() + dst.toUri().toString()); + LOG.info("Path to hdfs file: {}", fullPath); + + FileResolver resolver = FileResolver.resolve(fullPath); + assertTrue(resolver instanceof HdfsFileResolver); + assertEquals(fullPath, resolver.getURL()); + assertEquals("HelloWorld.jar", resolver.getFileName()); + assertEquals(origFileSize, getFileSize(resolver)); + + } catch (IOException e) { + throw new RuntimeException("Error setting up mini cluster", e); + } finally { + cluster.shutdown(); + } + } + +} diff --git a/modules/local-caching-classloader/src/test/java/test/HelloWorldTemplate b/modules/local-caching-classloader/src/test/java/test/HelloWorldTemplate new file mode 100644 index 0000000..c6def69 --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/test/HelloWorldTemplate @@ -0,0 +1,27 @@ +/* + * 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 test; + +public class HelloWorld { + + @Override + public String toString() { + return "%%"; + } +} diff --git a/modules/local-caching-classloader/src/test/java/test/Test.java b/modules/local-caching-classloader/src/test/java/test/Test.java new file mode 100644 index 0000000..3c2b196 --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/test/Test.java @@ -0,0 +1,27 @@ +/* + * 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 test; + +public interface Test { + + String hello(); + + int add(); + +} diff --git a/modules/local-caching-classloader/src/test/java/test/TestTemplate b/modules/local-caching-classloader/src/test/java/test/TestTemplate new file mode 100644 index 0000000..aa10a02 --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/test/TestTemplate @@ -0,0 +1,36 @@ +/* + * 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 test; + +public class TestObjectXXX implements Test { + + int i = 0; + + @Override + public String hello() { + return "Hello from XXX"; + } + + @Override + public int add() { + i += 1; + return i; + } + +} diff --git a/modules/local-caching-classloader/src/test/resources/log4j2-test.properties b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties new file mode 100644 index 0000000..37f4a7e --- /dev/null +++ b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties @@ -0,0 +1,35 @@ +# +# 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. +# + +status = info +dest = err +name = AccumuloVFSClassLoaderTestLoggingProperties + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.target = SYSTEM_OUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{ISO8601} [%-8c{2}] %-5p: %m%n + +logger.01.name = org.apache.accumulo.classloader.lcc +logger.01.level = trace + +rootLogger.level = warn +rootLogger.appenderRef.console.ref = STDOUT + diff --git a/modules/local-caching-classloader/src/test/shell/makeHelloWorldJars.sh b/modules/local-caching-classloader/src/test/shell/makeHelloWorldJars.sh new file mode 100755 index 0000000..c22e26d --- /dev/null +++ b/modules/local-caching-classloader/src/test/shell/makeHelloWorldJars.sh @@ -0,0 +1,35 @@ +#! /usr/bin/env bash +# +# 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. +# + +if [[ -z $JAVA_HOME ]]; then + echo "JAVA_HOME is not set. Java is required to proceed" + exit 1 +fi +mkdir -p target/generated-sources/HelloWorld/test +sed "s/%%/Hello World\!/" target/generated-sources/HelloWorld/test/HelloWorld.java +"$JAVA_HOME/bin/javac" target/generated-sources/HelloWorld/test/HelloWorld.java -d target/generated-sources/HelloWorld +"$JAVA_HOME/bin/jar" -cf target/test-classes/HelloWorld.jar -C target/generated-sources/HelloWorld test/HelloWorld.class +rm -r target/generated-sources/HelloWorld/test + +mkdir -p target/generated-sources/HalloWelt/test +sed "s/%%/Hallo Welt/" target/generated-sources/HalloWelt/test/HelloWorld.java +"$JAVA_HOME/bin/javac" target/generated-sources/HalloWelt/test/HelloWorld.java -d target/generated-sources/HalloWelt +"$JAVA_HOME/bin/jar" -cf target/test-classes/HelloWorld2.jar -C target/generated-sources/HalloWelt test/HelloWorld.class +rm -r target/generated-sources/HalloWelt/test diff --git a/modules/local-caching-classloader/src/test/shell/makeTestJars.sh b/modules/local-caching-classloader/src/test/shell/makeTestJars.sh new file mode 100755 index 0000000..7dfa64f --- /dev/null +++ b/modules/local-caching-classloader/src/test/shell/makeTestJars.sh @@ -0,0 +1,33 @@ +#! /usr/bin/env bash +# +# 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. +# + +if [[ -z $JAVA_HOME ]]; then + echo "JAVA_HOME is not set. Java is required to proceed" + exit 1 +fi + +for x in A B C D; do + mkdir -p target/generated-sources/$x/test target/test-classes/ClassLoaderTest$x + sed "s/XXX/$x/" target/generated-sources/$x/test/TestObject$x.java + "$JAVA_HOME/bin/javac" --source 11 --target 11 -cp target/test-classes target/generated-sources/$x/test/TestObject$x.java -d target/generated-sources/$x + "$JAVA_HOME/bin/jar" -cf target/test-classes/ClassLoaderTest$x/Test$x.jar -C target/generated-sources/$x test/TestObject$x.class + rm -r target/generated-sources/$x +done + From c4f658e6299a0bff130f27ee1573826cdd97c026 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Mon, 17 Nov 2025 12:43:48 +0000 Subject: [PATCH 07/31] Modify pom to run tests in forked VM serially --- modules/local-caching-classloader/pom.xml | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 3b7ce81..0db8b04 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -29,7 +29,19 @@ local-caching-classloader + --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.management/java.lang.management=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED ../../src/build/eclipse-codestyle.xml + + false + 1 + + false + true + + false + 1 + + false @@ -87,6 +99,36 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + ${surefire.forkCount} + ${surefire.reuseForks} + ${surefire.excludedGroups} + ${surefire.groups} + + ${project.build.directory} + + ${accumulo.build.extraTestArgs} + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${failsafe.forkCount} + ${failsafe.reuseForks} + ${failsafe.excludedGroups} + ${failsafe.groups} + + ${accumulo.it.uniq.test.dir} + ${project.build.directory} + + ${accumulo.build.extraTestArgs} + false + + org.codehaus.mojo exec-maven-plugin From 9b91a943ea53793b035c82e3df98df62caf260fb Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Mon, 17 Nov 2025 17:41:56 +0000 Subject: [PATCH 08/31] more tests, cleanup, added readme --- modules/local-caching-classloader/README.md | 69 ++++ modules/local-caching-classloader/pom.xml | 19 +- .../lcc/LocalCachingContextClassLoader.java | 128 +++--- ...LocalCachingContextClassLoaderFactory.java | 97 +++-- .../classloader/lcc/cache/CacheUtils.java | 7 +- .../lcc/definition/ContextDefinition.java | 36 +- .../classloader/lcc/definition/Resource.java | 22 +- .../lcc/resolvers/FileResolver.java | 18 + .../lcc/resolvers/HdfsFileResolver.java | 9 + .../lcc/resolvers/HttpFileResolver.java | 3 + .../lcc/resolvers/LocalFileResolver.java | 16 +- ...lCachingContextClassLoaderFactoryTest.java | 391 ++++++++++++++++++ .../LocalCachingContextClassLoaderTest.java | 36 +- .../accumulo/classloader/lcc/TestUtils.java | 3 + .../classloader/lcc/cache/CacheUtilsTest.java | 17 +- .../src/test/resources/log4j2-test.properties | 5 +- 16 files changed, 747 insertions(+), 129 deletions(-) create mode 100644 modules/local-caching-classloader/README.md create mode 100644 modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md new file mode 100644 index 0000000..fc8f0d4 --- /dev/null +++ b/modules/local-caching-classloader/README.md @@ -0,0 +1,69 @@ + + +# Local Caching ClassLoader + +The LocalCachingContextClassLoaderFactory is an Accumulo ContextClassLoaderFactory implementation that creates and maintains a +LocalCachingContextClassLoader. The `LocalCachingContextClassLoaderFactory.getClassLoader(String)` method expects the method +argument to be a valid `file`, `hdfs`, `http` or `https` URL to a context definition file. + +The context definition file is a JSON formatted file that contains the name of the context, the interval in seconds at which +the context definition file should be monitored, and a list of classpath resources. The LocalCachingContextClassLoaderFactory +creates the LocalCachingContextClassLoader based on the initial contents of the context definition file, and updates the classloader +as changes are noticed based on the monitoring interval. An example of the context definition file is below. + +``` +{ + "contextName": "myContext", + "monitorIntervalSeconds": 5, + "resources": [ + { + "location": "file:/home/user/ClassLoaderTestA/TestA.jar", + "checksum": "a10883244d70d971ec25cbfa69b6f08f" + }, + { + "location": "hdfs://localhost:8020/contextB/TestB.jar", + "checksum": "a02a3b7026528156fb782dcdecaaa097" + }, + { + "location": "http://localhost:80/TestC.jar", + "checksum": "f464e66f6d07a41c656e8f4679509215" + } + ] +} +``` + +The system property `accumulo.classloader.cache.dir` is required to be set to a local directory on the host. The +LocalCachingContextClassLoader creates a directory at this location for each named context. Each context cache directory +contains a lock file and a copy of each fetched resource that is named in the context definition file using the format: +`fileName_checksum`. The lock file is used with Java's `FileChannel.tryLock` to enable exclusive access (on supported +platforms) to the directory from different processes on the same host. + +## 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. It is left to the user to remove unused context cache directories and unused old files within a context cache directory. + +## Accumulo Configuration + +To use this with Accumulo: + + 1. Set the following Accumulo site property: `general.context.class.loader.factory=org.apache.accumulo.classloader.lcc.LocalCachingContextClassLoaderFactory` + + 2. Set the following table property: `table.class.loader.context=(file|hdfs|http|https)://path/to/context/definition.json` + + diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 0db8b04..8193b37 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -28,6 +28,7 @@ ../../pom.xml local-caching-classloader + classloader-extras-local-caching --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.management/java.lang.management=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED ../../src/build/eclipse-codestyle.xml @@ -44,6 +45,12 @@ false + + + com.github.spotbugs + spotbugs-annotations + true + commons-codec commons-codec @@ -55,6 +62,12 @@ accumulo-core provided + + + org.slf4j + slf4j-api + provided + org.apache.hadoop hadoop-client-minicluster @@ -90,12 +103,6 @@ junit-jupiter-api test - - - - - - diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java index 4808abe..be0d037 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java @@ -24,6 +24,9 @@ import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.Objects; @@ -42,7 +45,7 @@ public class LocalCachingContextClassLoader { - public static class ClassPathElement { + private static class ClassPathElement { private final FileResolver resolver; private final URL localCachedCopyLocation; private final String localCachedCopyDigest; @@ -56,6 +59,7 @@ public ClassPathElement(FileResolver resolver, URL localCachedCopy, Objects.requireNonNull(localCachedCopyDigest, "local cached copy md5 must be supplied"); } + @SuppressWarnings("unused") public FileResolver getResolver() { return resolver; } @@ -64,31 +68,36 @@ public URL getLocalCachedCopyLocation() { return localCachedCopyLocation; } + @SuppressWarnings("unused") public String getLocalCachedCopyDigest() { return localCachedCopyDigest; } - } - private ClassPathElement cacheResource(final Resource resource) throws Exception { + @Override + public int hashCode() { + return Objects.hash(localCachedCopyDigest, localCachedCopyLocation, resolver); + } - final FileResolver source = FileResolver.resolve(resource.getURL()); - final Path cacheLocation = - contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum()); - final File cacheFile = cacheLocation.toFile(); - if (!Files.exists(cacheLocation)) { - try (InputStream is = source.getInputStream()) { - Files.copy(is, cacheLocation); - } - final String checksum = Constants.getChecksummer().digestAsHex(cacheFile); - if (!resource.getChecksum().equals(checksum)) { - // TODO: What we just wrote does not match the MD5 in the Resource description. - } - return new ClassPathElement(source, cacheFile.toURI().toURL(), checksum); - } else { - // File exists, return new ClassPathElement based on existing file - String fileName = cacheFile.getName(); - String[] parts = fileName.split("_"); - return new ClassPathElement(source, cacheFile.toURI().toURL(), parts[1]); + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ClassPathElement other = (ClassPathElement) obj; + return Objects.equals(localCachedCopyDigest, other.localCachedCopyDigest) + && Objects.equals(localCachedCopyLocation, other.localCachedCopyLocation) + && Objects.equals(resolver, other.resolver); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("source: ").append(resolver.getURL()); + buf.append(", cached copy:").append(localCachedCopyLocation); + return buf.toString(); } } @@ -108,10 +117,48 @@ public LocalCachingContextClassLoader(ContextDefinition contextDefinition) this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(contextName); } + @Deprecated + @Override + protected final void finalize() { + /* + * unused; this is final due to finalizer attacks since the constructor throws exceptions (see + * spotbugs CT_CONSTRUCTOR_THROW) + */ + } + public ContextDefinition getDefinition() { return definition.get(); } + private ClassPathElement cacheResource(final Resource resource) throws Exception { + + final FileResolver source = FileResolver.resolve(resource.getURL()); + final Path cacheLocation = + contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum()); + final File cacheFile = cacheLocation.toFile(); + if (!Files.exists(cacheLocation)) { + LOG.trace("Caching resource {} at {}", source.getURL(), cacheFile.getAbsolutePath()); + try (InputStream is = source.getInputStream()) { + Files.copy(is, cacheLocation); + } + final String checksum = Constants.getChecksummer().digestAsHex(cacheFile); + if (!resource.getChecksum().equals(checksum)) { + LOG.error("Checksum {} for resource {} does not match checksum in context definition {}", + checksum, source.getURL(), resource.getChecksum()); + throw new IllegalStateException("Checksum " + checksum + " for resource " + source.getURL() + + " does not match checksum in context definition " + resource.getChecksum()); + } + return new ClassPathElement(source, cacheFile.toURI().toURL(), checksum); + } else { + // File exists, return new ClassPathElement based on existing file + LOG.trace("Resource {} is already cached at {}", source.getURL(), + cacheFile.getAbsolutePath()); + String fileName = cacheFile.getName(); + String[] parts = fileName.split("_"); + return new ClassPathElement(source, cacheFile.toURI().toURL(), parts[1]); + } + } + public void initialize() { try { synchronized (elements) { @@ -147,17 +194,10 @@ public void update(final ContextDefinition update) { return; } try { + elements.clear(); for (Resource updatedResource : update.getResources()) { - ClassPathElement existing = findElementBySourceLocation(updatedResource.getURL()); - if (existing == null) { - // new resource - ClassPathElement cpe = cacheResource(updatedResource); - addElement(cpe); - } else if (existing.getLocalCachedCopyDigest().equals(updatedResource.getChecksum())) { - removeElement(existing); - ClassPathElement cpe = cacheResource(updatedResource); - addElement(cpe); - } + ClassPathElement cpe = cacheResource(updatedResource); + addElement(cpe); } this.definition.set(update); } finally { @@ -169,39 +209,31 @@ public void update(final ContextDefinition update) { } } - private ClassPathElement findElementBySourceLocation(URL source) { - for (ClassPathElement cpe : elements) { - if (cpe.getResolver().getURL().equals(source)) { - return cpe; - } - } - return null; - } - - private void removeElement(ClassPathElement element) { - synchronized (elements) { - elements.remove(element); - elementsChanged.set(true); - } - } - private void addElement(ClassPathElement element) { synchronized (elements) { elements.add(element); elementsChanged.set(true); + LOG.trace("Added element {} to classpath", element); } } public ClassLoader getClassloader() { synchronized (elements) { if (classloader.get() == null || elementsChanged.get()) { + LOG.trace("Class path contents have changed, creating new classloader"); URL[] urls = new URL[elements.size()]; Iterator iter = elements.iterator(); for (int x = 0; x < elements.size(); x++) { urls[x] = iter.next().getLocalCachedCopyLocation(); } elementsChanged.set(false); - classloader.set(new URLClassLoader(contextName, urls, this.getClass().getClassLoader())); + final URLClassLoader cl = + AccessController.doPrivileged((PrivilegedAction) () -> { + return new URLClassLoader(contextName, urls, this.getClass().getClassLoader()); + }); + classloader.set(cl); + LOG.trace("New classloader created from URLs: {}", + Arrays.asList(classloader.get().getURLs())); } } return classloader.get(); 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 ff20b98..538b213 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 @@ -18,6 +18,8 @@ */ package org.apache.accumulo.classloader.lcc; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -37,30 +39,28 @@ /** * A ContextClassLoaderFactory implementation that creates and maintains a ClassLoader for a named - * context. This factory expects the parameter passed to {@code {@link #getClassLoader(String)} to - * be the URL of a json formatted {@link #ContextDefinition} file. The file contains an interval at - * which this class should monitor the file for changes and a list of {@link Resource} objects. Each + * context. This factory expects the parameter passed to {@link #getClassLoader(String)} to be the + * URL of a json formatted {@link #ContextDefinition} file. The file contains an interval at which + * this class should monitor the file for changes and a list of {@link Resource} objects. Each * resource is defined by a URL to the file and an expected MD5 hash value. - * + *

* The URLs supplied for the context definition file and for the resources can use one of the * following protocols: file://, http://, or hdfs://. - * + *

* As this class processes the ContextDefinition it fetches the contents of the resource from the * resource URL and caches it in a directory on the local filesystem. This class uses the value of - * the system property {@code Constants#CACHE_DIR_PROPERTY} as the root directory and creates a + * the system property {@link Constants#CACHE_DIR_PROPERTY} as the root directory and creates a * sub-directory for each context name. Each context cache directory contains a lock file and a copy - * of each fetched resource that is named using the following format: - * fileName_md5Hash.fileNameSuffix. - * + * of each fetched resource that is named using the following format: fileName_checksum. + *

* The lock file prevents processes from manipulating the contexts of the context cache directory * concurrently, which enables the cache directories to be shared among multiple processes on the * host. - * + *

* Note that 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. - * */ public class LocalCachingContextClassLoaderFactory implements ContextClassLoaderFactory { @@ -75,7 +75,13 @@ private ContextDefinition parseContextDefinition(URL url) throws ContextClassLoa FileResolver resolver = FileResolver.resolve(url); try { try (InputStream is = resolver.getInputStream()) { - return Constants.GSON.fromJson(new InputStreamReader(is), ContextDefinition.class); + ContextDefinition def = + Constants.GSON.fromJson(new InputStreamReader(is, UTF_8), ContextDefinition.class); + if (def == null) { + throw new ContextClassLoaderException( + "ContextDefinition null for context definition file: " + resolver.getURL()); + } + return def; } } catch (IOException e) { throw new ContextClassLoaderException( @@ -83,36 +89,37 @@ private ContextDefinition parseContextDefinition(URL url) throws ContextClassLoa } } - private void monitorContext(final String contextLocation) { + private void monitorContext(final String contextLocation, boolean initialCall, int interval) { final SoftReference ccl = contexts.get(contextLocation); - if (ccl == null) { + if (!initialCall && ccl == null) { // context has been removed from the map, no need to check for update return; } - final LocalCachingContextClassLoader classLoader = ccl.get(); - if (classLoader == null) { + if (!initialCall && ccl.get() == null) { // classloader has been garbage collected. Remove from the map and return contexts.remove(contextLocation); return; } - final ContextDefinition currentDef = classLoader.getDefinition(); Constants.EXECUTOR.schedule(() -> { + final LocalCachingContextClassLoader classLoader = contexts.get(contextLocation).get(); + final ContextDefinition currentDef = classLoader.getDefinition(); try { - URL contextManifest = new URL(contextLocation); + final URL contextManifest = new URL(contextLocation); final ContextDefinition update = parseContextDefinition(contextManifest); if (!Arrays.equals(currentDef.getChecksum(), update.getChecksum())) { - LOG.debug("Context defintion for {} has changed", currentDef.getContextName()); + LOG.debug("Context definition for {} has changed", currentDef.getContextName()); classLoader.update(update); } else { - LOG.debug("Context defintion for {} has not changed", currentDef.getContextName()); + LOG.debug("Context definition for {} has not changed", currentDef.getContextName()); } - monitorContext(contextLocation); + monitorContext(contextLocation, false, update.getMonitorIntervalSeconds()); } catch (Exception e) { - LOG.error("Error parsing context definition at {}", contextLocation); + LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", + contextLocation, e); } - }, currentDef.getMonitorIntervalSeconds(), TimeUnit.SECONDS); - LOG.debug("Monitoring context definition file {} for changes at {} second intervals", - contextLocation, currentDef.getMonitorIntervalSeconds()); + }, interval, TimeUnit.SECONDS); + LOG.trace("Monitoring context definition file {} for changes at {} second intervals", + contextLocation, interval); } @Override @@ -121,21 +128,16 @@ public ClassLoader getClassLoader(final String contextLocation) try { SoftReference ccl = contexts.computeIfAbsent(contextLocation, cn -> { - try { - URL contextManifest = new URL(contextLocation); - CacheUtils.createBaseCacheDir(); - ContextDefinition m = parseContextDefinition(contextManifest); - LocalCachingContextClassLoader newCcl = new LocalCachingContextClassLoader(m); - newCcl.initialize(); - monitorContext(contextLocation); - return new SoftReference<>(newCcl); - } catch (MalformedURLException e) { - throw new RuntimeException("Expected valid URL to context definition file", e); - } catch (ContextClassLoaderException e) { - throw new RuntimeException("Error processing context definition", e); - } + return new SoftReference<>(createContextClassLoader(contextLocation)); }); - return ccl.get().getClassloader(); + final LocalCachingContextClassLoader lcccl = ccl.get(); + if (ccl.get() == null) { + // SoftReference is in the map, but the classloader has been garbage collected. + // Need to recreate it + contexts.remove(contextLocation); + return getClassLoader(contextLocation); + } + return lcccl.getClassloader(); } catch (RuntimeException re) { Throwable t = re.getCause(); if (t != null && t instanceof ContextClassLoaderException) { @@ -146,4 +148,21 @@ public ClassLoader getClassLoader(final String contextLocation) } } + private LocalCachingContextClassLoader createContextClassLoader(final String contextLocation) { + try { + URL contextManifest = new URL(contextLocation); + CacheUtils.createBaseCacheDir(); + ContextDefinition m = parseContextDefinition(contextManifest); + LocalCachingContextClassLoader newCcl = new LocalCachingContextClassLoader(m); + newCcl.initialize(); + monitorContext(contextLocation, true, m.getMonitorIntervalSeconds()); + return newCcl; + } catch (MalformedURLException e) { + throw new RuntimeException( + "Expected valid URL to context definition file but received: " + contextLocation, e); + } catch (ContextClassLoaderException e) { + throw new RuntimeException("Error processing context definition", e); + } + } + } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index 93377af..f4a5fd6 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -44,6 +44,8 @@ import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class CacheUtils { private static final Set CACHE_DIR_PERMS = @@ -62,11 +64,12 @@ public LockInfo(FileChannel channel, FileLock lock) { this.lock = Objects.requireNonNull(lock, "lock must be supplied"); } - public FileChannel getChannel() { + @SuppressFBWarnings(value = "EI_EXPOSE_REP") + FileChannel getChannel() { return channel; } - public FileLock getLock() { + FileLock getLock() { return lock; } 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 f30a4dc..b1bb0e1 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 @@ -24,17 +24,26 @@ import org.apache.accumulo.classloader.lcc.Constants; +import com.google.common.base.Preconditions; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +@SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) public class ContextDefinition { - private final String contextName; - private final int monitorIntervalSeconds; - private final List resources; + private String contextName; + private int monitorIntervalSeconds; + private List resources; private volatile transient byte[] checksum = null; + public ContextDefinition() {} + public ContextDefinition(String contextName, int monitorIntervalSeconds, List resources) { - this.contextName = contextName; + this.contextName = Objects.requireNonNull(contextName, "context name must be supplied"); + Preconditions.checkArgument(monitorIntervalSeconds > 0, + "monitor interval must be greater than zero"); this.monitorIntervalSeconds = monitorIntervalSeconds; - this.resources = resources; + this.resources = Objects.requireNonNull(resources, "resources must be supplied"); } public String getContextName() { @@ -49,6 +58,18 @@ public List getResources() { return resources; } + public void setContextName(String contextName) { + this.contextName = contextName; + } + + public void setMonitorIntervalSeconds(int monitorIntervalSeconds) { + this.monitorIntervalSeconds = monitorIntervalSeconds; + } + + public void setResources(List resources) { + this.resources = resources; + } + @Override public int hashCode() { return Objects.hash(contextName, monitorIntervalSeconds, resources); @@ -70,9 +91,12 @@ public boolean equals(Object obj) { public synchronized byte[] getChecksum() throws NoSuchAlgorithmException { if (checksum == null) { - checksum = Constants.getChecksummer().digest(Constants.GSON.toJson(this)); + checksum = Constants.getChecksummer().digest(toJson()); } return checksum; } + public String toJson() { + return Constants.GSON.toJson(this); + } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java index a1e891e..c906818 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java @@ -24,18 +24,16 @@ public class Resource { - private final String location; - private final String checksum; + private String location; + private String checksum; + + public Resource() {} public Resource(String location, String checksum) { this.location = location; this.checksum = checksum; } - public URL getURL() throws MalformedURLException { - return new URL(location); - } - public String getLocation() { return location; } @@ -44,6 +42,18 @@ public String getChecksum() { return checksum; } + public void setLocation(String location) { + this.location = location; + } + + public void setChecksum(String checksum) { + this.checksum = checksum; + } + + public URL getURL() throws MalformedURLException { + return new URL(location); + } + @Override public int hashCode() { return Objects.hash(checksum, location); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java index 6466a68..9c4aecb 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java @@ -21,6 +21,7 @@ import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; +import java.util.Objects; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; @@ -55,4 +56,21 @@ public URL getURL() { public abstract InputStream getInputStream() throws ContextClassLoaderException; + @Override + public int hashCode() { + return Objects.hash(url); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FileResolver other = (FileResolver) obj; + return Objects.equals(url, other.url); + } + } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java index 9f9b193..a2b59cb 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java @@ -51,6 +51,15 @@ protected HdfsFileResolver(URL url) throws ContextClassLoaderException { } } + @Deprecated + @Override + protected final void finalize() { + /* + * unused; this is final due to finalizer attacks since the constructor throws exceptions (see + * spotbugs CT_CONSTRUCTOR_THROW) + */ + } + @Override public String getFileName() throws URISyntaxException { return this.path.getName(); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java index 1b2ce2e..1b491bb 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java @@ -25,6 +25,8 @@ import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class HttpFileResolver extends FileResolver { protected HttpFileResolver(URL url) throws ContextClassLoaderException { @@ -38,6 +40,7 @@ public String getFileName() throws URISyntaxException { } @Override + @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD") public InputStream getInputStream() throws ContextClassLoaderException { try { return url.openStream(); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java index 8abfa64..c3d0e57 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java @@ -53,9 +53,23 @@ public LocalFileResolver(URL url) throws ContextClassLoaderException { } } + @Deprecated + @Override + protected final void finalize() { + /* + * unused; this is final due to finalizer attacks since the constructor throws exceptions (see + * spotbugs CT_CONSTRUCTOR_THROW) + */ + } + @Override public String getFileName() throws URISyntaxException { - return Paths.get(getURL().toURI()).getFileName().toString(); + Path filePath = Paths.get(getURL().toURI()).getFileName(); + if (filePath != null) { + return filePath.toString(); + } else { + return null; + } } @Override 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 new file mode 100644 index 0000000..1ee1884 --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java @@ -0,0 +1,391 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; + +import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; +import org.apache.accumulo.classloader.lcc.definition.Resource; +import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hdfs.MiniDFSCluster; +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class LocalCachingContextClassLoaderFactoryTest { + + private static final int MONITOR_INTERVAL_SECS = 5; + private static MiniDFSCluster hdfs; + private static FileSystem fs; + private static Server jetty; + private static URL jarAOrigLocation; + private static URL jarBOrigLocation; + private static URL jarCOrigLocation; + private static URL jarDOrigLocation; + private static URL localAllContext; + private static URL hdfsAllContext; + private static URL jettyAllContext; + + @TempDir + private static java.nio.file.Path tempDir; + + @BeforeAll + public static void beforeAll() throws Exception { + String tmp = tempDir.resolve("base").toUri().toString(); + System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); + + // Find the Test jar files + jarAOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestA/TestA.jar"); + assertNotNull(jarAOrigLocation); + jarBOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestB/TestB.jar"); + assertNotNull(jarBOrigLocation); + jarCOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestC/TestC.jar"); + assertNotNull(jarCOrigLocation); + jarDOrigLocation = + LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestD/TestD.jar"); + assertNotNull(jarDOrigLocation); + + // Put B into HDFS + hdfs = TestUtils.getMiniCluster(); + URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory(hdfs.getConfiguration(0))); + + fs = hdfs.getFileSystem(); + assertTrue(fs.mkdirs(new Path("/contextB"))); + final Path dst = new Path("/contextB/TestB.jar"); + fs.copyFromLocalFile(new Path(jarBOrigLocation.toURI()), dst); + assertTrue(fs.exists(dst)); + final URL jarBHdfsLocation = new URL(fs.getUri().toString() + dst.toUri().toString()); + + // Put C into Jetty + java.nio.file.Path jarCParentDirectory = Paths.get(jarCOrigLocation.toURI()).getParent(); + assertNotNull(jarCParentDirectory); + jetty = TestUtils.getJetty(jarCParentDirectory); + final URL jarCJettyLocation = jetty.getURI().resolve("TestC.jar").toURL(); + + // ContextDefinition with all jars + ContextDefinition allJarsDef = createContextDef("all", jarAOrigLocation, jarBHdfsLocation, + jarCJettyLocation, jarDOrigLocation); + String allJarsDefJson = allJarsDef.toJson(); + System.out.println(allJarsDefJson); + + File localDefFile = new File(jarCParentDirectory.toFile(), "allContextDefinition.json"); + Files.writeString(localDefFile.toPath(), allJarsDefJson, StandardOpenOption.CREATE); + assertTrue(Files.exists(localDefFile.toPath())); + + Path hdfsDefFile = new Path("/allContextDefinition.json"); + fs.copyFromLocalFile(new Path(localDefFile.toURI()), hdfsDefFile); + assertTrue(fs.exists(hdfsDefFile)); + + localAllContext = localDefFile.toURI().toURL(); + hdfsAllContext = new URL(fs.getUri().toString() + hdfsDefFile.toUri().toString()); + jettyAllContext = jetty.getURI().resolve("allContextDefinition.json").toURL(); + + } + + @AfterAll + public static void afterAll() throws Exception { + if (jetty != null) { + jetty.stop(); + jetty.join(); + } + if (hdfs != null) { + hdfs.shutdown(); + } + } + + @Test + public void testInvalidContextDefinitionURL() { + LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); + ContextClassLoaderException ex = + assertThrows(ContextClassLoaderException.class, () -> factory.getClassLoader("/not/a/URL")); + assertEquals("Error getting classloader for context: Expected valid URL to context definition " + + "file but received: /not/a/URL", ex.getMessage()); + } + + @Test + public void testInitialContextDefinitionEmpty() throws Exception { + // Create a new context definition file in HDFS, but with no content + assertTrue(fs.mkdirs(new Path("/contextDefs"))); + final Path empty = new Path("/contextDefs/EmptyContextDefinitionFile.json"); + assertTrue(fs.createNewFile(empty)); + assertTrue(fs.exists(empty)); + final URL emptyDefUrl = new URL(fs.getUri().toString() + empty.toUri().toString()); + + LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); + ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, + () -> factory.getClassLoader(emptyDefUrl.toString())); + assertEquals( + "Error getting classloader for context: ContextDefinition null for context definition " + + "file: " + emptyDefUrl.toString(), + ex.getMessage()); + } + + @Test + public void testInitialContextDefinitionInvalid() throws Exception { + // Create a new context definition file in HDFS, but with invalid content + assertTrue(fs.mkdirs(new Path("/contextDefs"))); + final Path invalid = new Path("/contextDefs/InvalidContextDefinitionFile.json"); + try (FSDataOutputStream out = fs.create(invalid)) { + ContextDefinition def = createContextDef("invalid", jarAOrigLocation); + out.writeBytes(def.toJson().substring(0, 4)); + } + assertTrue(fs.exists(invalid)); + final URL invalidDefUrl = new URL(fs.getUri().toString() + invalid.toUri().toString()); + + LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); + ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, + () -> factory.getClassLoader(invalidDefUrl.toString())); + assertTrue(ex.getMessage().startsWith( + "Error getting classloader for context: com.google.gson.stream.MalformedJsonException")); + } + + @Test + public void testInitial() throws Exception { + // Create a new context definition file in HDFS, but with invalid content + assertTrue(fs.mkdirs(new Path("/contextDefs"))); + final Path initial = new Path("/contextDefs/InitialContextDefinitionFile.json"); + try (FSDataOutputStream out = fs.create(initial)) { + ContextDefinition def = createContextDef("initial", jarAOrigLocation); + out.writeBytes(def.toJson()); + } + assertTrue(fs.exists(initial)); + final URL initialDefUrl = new URL(fs.getUri().toString() + initial.toUri().toString()); + + LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); + ClassLoader cl = factory.getClassLoader(initialDefUrl.toString()); + @SuppressWarnings("unchecked") + Class clazzA = + (Class) cl.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + } + + @Test + public void testUpdate() throws Exception { + // Create a new context definition file in HDFS + assertTrue(fs.mkdirs(new Path("/contextDefs"))); + final Path defFilePath = new Path("/contextDefs/UpdateContextDefinitionFile.json"); + final ContextDefinition def = createContextDef("update", jarAOrigLocation); + try (FSDataOutputStream out = fs.create(defFilePath)) { + out.writeBytes(def.toJson()); + } + assertTrue(fs.exists(defFilePath)); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); + + final LocalCachingContextClassLoaderFactory factory = + new LocalCachingContextClassLoaderFactory(); + final ClassLoader cl = factory.getClassLoader(updateDefUrl.toString()); + @SuppressWarnings("unchecked") + Class clazzA = + (Class) cl.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + + // Update the contents of the context definition json file + fs.delete(defFilePath, false); + assertFalse(fs.exists(defFilePath)); + + ContextDefinition updateDef = createContextDef("update", jarDOrigLocation); + try (FSDataOutputStream out = fs.create(defFilePath)) { + out.writeBytes(updateDef.toJson()); + } + assertTrue(fs.exists(defFilePath)); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl2 = factory.getClassLoader(updateDefUrl.toString()); + assertThrows(ClassNotFoundException.class, () -> cl2.loadClass("test.TestObjectA")); + + @SuppressWarnings("unchecked") + Class clazzD = + (Class) cl2.loadClass("test.TestObjectD"); + test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); + assertEquals("Hello from D", d1.hello()); + + } + + @Test + public void testUpdateInvalid() throws Exception { + // Create a new context definition file in HDFS, but with invalid content + assertTrue(fs.mkdirs(new Path("/contextDefs"))); + final Path defFilePath = new Path("/contextDefs/UpdateContextDefinitionFile.json"); + final ContextDefinition def = createContextDef("update", jarAOrigLocation); + try (FSDataOutputStream out = fs.create(defFilePath)) { + out.writeBytes(def.toJson()); + } + assertTrue(fs.exists(defFilePath)); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); + + final LocalCachingContextClassLoaderFactory factory = + new LocalCachingContextClassLoaderFactory(); + final ClassLoader cl = factory.getClassLoader(updateDefUrl.toString()); + @SuppressWarnings("unchecked") + Class clazzA = + (Class) cl.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + + // Update the contents of the context definition json file + fs.delete(defFilePath, false); + assertFalse(fs.exists(defFilePath)); + + ContextDefinition updateDef = createContextDef("update", jarDOrigLocation); + try (FSDataOutputStream out = fs.create(defFilePath)) { + out.writeBytes(updateDef.toJson().substring(0, 4)); + } + assertTrue(fs.exists(defFilePath)); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl2 = factory.getClassLoader(updateDefUrl.toString()); + + @SuppressWarnings("unchecked") + Class clazzA2 = + (Class) cl2.loadClass("test.TestObjectA"); + test.Test a2 = clazzA2.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a2.hello()); + assertEquals(clazzA, clazzA2); + + assertThrows(ClassNotFoundException.class, () -> cl2.loadClass("test.TestObjectD")); + } + + @SuppressWarnings("unchecked") + @Test + public void testCreateFromLocal() throws Exception { + LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); + final ClassLoader cl = factory.getClassLoader(localAllContext.toString()); + + final Class clazzA = + (Class) cl.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + + final Class clazzB = + (Class) cl.loadClass("test.TestObjectB"); + test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); + assertEquals("Hello from B", b1.hello()); + + final Class clazzC = + (Class) cl.loadClass("test.TestObjectC"); + test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); + assertEquals("Hello from C", c1.hello()); + + final Class clazzD = + (Class) cl.loadClass("test.TestObjectD"); + test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); + assertEquals("Hello from D", d1.hello()); + + } + + @SuppressWarnings("unchecked") + @Test + public void testCreateFromHdfs() throws Exception { + LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); + final ClassLoader cl = factory.getClassLoader(hdfsAllContext.toString()); + + final Class clazzA = + (Class) cl.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + + final Class clazzB = + (Class) cl.loadClass("test.TestObjectB"); + test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); + assertEquals("Hello from B", b1.hello()); + + final Class clazzC = + (Class) cl.loadClass("test.TestObjectC"); + test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); + assertEquals("Hello from C", c1.hello()); + + final Class clazzD = + (Class) cl.loadClass("test.TestObjectD"); + test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); + assertEquals("Hello from D", d1.hello()); + + } + + @SuppressWarnings("unchecked") + @Test + public void testCreateFromHttp() throws Exception { + LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); + final ClassLoader cl = factory.getClassLoader(jettyAllContext.toString()); + + final Class clazzA = + (Class) cl.loadClass("test.TestObjectA"); + test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); + assertEquals("Hello from A", a1.hello()); + + final Class clazzB = + (Class) cl.loadClass("test.TestObjectB"); + test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); + assertEquals("Hello from B", b1.hello()); + + final Class clazzC = + (Class) cl.loadClass("test.TestObjectC"); + test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); + assertEquals("Hello from C", c1.hello()); + + final Class clazzD = + (Class) cl.loadClass("test.TestObjectD"); + test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); + assertEquals("Hello from D", d1.hello()); + + } + + private static ContextDefinition createContextDef(String contextName, URL... sources) + throws ContextClassLoaderException, IOException { + List resources = new ArrayList<>(); + for (URL u : sources) { + FileResolver resolver = FileResolver.resolve(u); + try (InputStream is = resolver.getInputStream()) { + String checksum = Constants.getChecksummer().digestAsHex(is); + resources.add(new Resource(u.toString(), checksum)); + } + } + return new ContextDefinition(contextName, MONITOR_INTERVAL_SECS, resources); + } + +} diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java index 60da08c..8db23f9 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java @@ -20,7 +20,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -151,19 +153,19 @@ public void testUpdate() throws Exception { LocalCachingContextClassLoader lcccl = new LocalCachingContextClassLoader(def); lcccl.initialize(); - ClassLoader contextClassLoader = lcccl.getClassloader(); + final ClassLoader contextClassLoader = lcccl.getClassloader(); - Class clazzA = + final Class clazzA = (Class) contextClassLoader.loadClass("test.TestObjectA"); test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); assertEquals("Hello from A", a1.hello()); - Class clazzB = + final Class clazzB = (Class) contextClassLoader.loadClass("test.TestObjectB"); test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); assertEquals("Hello from B", b1.hello()); - Class clazzC = + final Class clazzC = (Class) contextClassLoader.loadClass("test.TestObjectC"); test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); assertEquals("Hello from C", c1.hello()); @@ -194,25 +196,25 @@ public void testUpdate() throws Exception { assertTrue(Files.exists(base.resolve(CONTEXT_NAME).resolve(filename + "_" + checksum))); } - contextClassLoader = lcccl.getClassloader(); + final ClassLoader updatedContextClassLoader = lcccl.getClassloader(); - clazzA = (Class) contextClassLoader.loadClass("test.TestObjectA"); - test.Test a2 = clazzA.getDeclaredConstructor().newInstance(); + final Class clazzA2 = + (Class) updatedContextClassLoader.loadClass("test.TestObjectA"); + test.Test a2 = clazzA2.getDeclaredConstructor().newInstance(); + assertNotEquals(clazzA, clazzA2); assertEquals("Hello from A", a2.hello()); - clazzB = (Class) contextClassLoader.loadClass("test.TestObjectB"); - test.Test b2 = clazzB.getDeclaredConstructor().newInstance(); + final Class clazzB2 = + (Class) updatedContextClassLoader.loadClass("test.TestObjectB"); + test.Test b2 = clazzB2.getDeclaredConstructor().newInstance(); + assertNotEquals(clazzB, clazzB2); assertEquals("Hello from B", b2.hello()); - // Class C already loaded in the Virtual Machine so this will work even though - // removed from context. When the Garbage Collector unloads this class then - // TestObjectC will not work anymore. - clazzC = (Class) contextClassLoader.loadClass("test.TestObjectC"); - test.Test c2 = clazzC.getDeclaredConstructor().newInstance(); - assertEquals("Hello from C", c2.hello()); + assertThrows(ClassNotFoundException.class, + () -> updatedContextClassLoader.loadClass("test.TestObjectC")); - Class clazzD = - (Class) contextClassLoader.loadClass("test.TestObjectD"); + final Class clazzD = + (Class) updatedContextClassLoader.loadClass("test.TestObjectD"); test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); assertEquals("Hello from D", d1.hello()); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java index 2992661..f8b38ab 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java @@ -33,6 +33,8 @@ import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.util.resource.PathResource; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class TestUtils { private static String computeDatanodeDirectoryPermission() { @@ -91,6 +93,7 @@ public static Server getJetty(Path resourceDirectory) throws Exception { return jetty; } + @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD") public static String computeResourceChecksum(URL resourceLocation) throws IOException { try (InputStream is = resourceLocation.openStream()) { return Constants.getChecksummer().digestAsHex(is); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java index ac928fc..997638a 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java @@ -18,9 +18,11 @@ */ package org.apache.accumulo.classloader.lcc.cache; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Files; @@ -31,7 +33,7 @@ import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.classloader.lcc.cache.CacheUtils.LockInfo; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; @@ -44,13 +46,22 @@ public class CacheUtilsTest { @TempDir private static Path tempDir; - @BeforeAll - public static void beforeAll() { + @BeforeEach + public void beforeEach() { String tmp = tempDir.resolve("base").toUri().toString(); LOG.info("Setting cache base directory to {}", tmp); System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); } + @Test + public void testPropertyNotSet() { + System.clearProperty(Constants.CACHE_DIR_PROPERTY); + ContextClassLoaderException ex = + assertThrows(ContextClassLoaderException.class, () -> CacheUtils.createBaseCacheDir()); + assertEquals("Error getting classloader for context: System property " + + Constants.CACHE_DIR_PROPERTY + " not set.", ex.getMessage()); + } + @Test public void testCreateBaseDir() throws Exception { final Path base = Paths.get(tempDir.resolve("base").toUri()); diff --git a/modules/local-caching-classloader/src/test/resources/log4j2-test.properties b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties index 37f4a7e..9a58884 100644 --- a/modules/local-caching-classloader/src/test/resources/log4j2-test.properties +++ b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties @@ -19,7 +19,7 @@ status = info dest = err -name = AccumuloVFSClassLoaderTestLoggingProperties +name = LocalCachineClassLoaderTestLoggingProperties appender.console.type = Console appender.console.name = STDOUT @@ -30,6 +30,9 @@ appender.console.layout.pattern = %d{ISO8601} [%-8c{2}] %-5p: %m%n logger.01.name = org.apache.accumulo.classloader.lcc logger.01.level = trace +logger.02.name = org.apache.hadoop +logger.02.level = error + rootLogger.level = warn rootLogger.appenderRef.console.ref = STDOUT From 375114cc100eda0b3e5fffe16f6deefef9119636 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Mon, 17 Nov 2025 19:00:42 +0000 Subject: [PATCH 09/31] Fixed build issues --- modules/local-caching-classloader/pom.xml | 35 +++++++++++++------ .../src/test/shell/makeTestJars.sh | 2 +- pom.xml | 1 - 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 8193b37..321ed89 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -51,17 +51,42 @@ spotbugs-annotations true + + com.google.code.gson + gson + provided + + + com.google.guava + guava + provided + commons-codec commons-codec provided + + commons-io + commons-io + provided + org.apache.accumulo accumulo-core provided + + org.apache.hadoop + hadoop-client-api + provided + + + org.apache.hadoop + hadoop-client-runtime + provided + org.slf4j @@ -78,21 +103,11 @@ log4j-slf4j2-impl test - - org.eclipse.jetty - jetty-io - test - org.eclipse.jetty jetty-server test - - org.eclipse.jetty - jetty-servlet - test - org.eclipse.jetty jetty-util diff --git a/modules/local-caching-classloader/src/test/shell/makeTestJars.sh b/modules/local-caching-classloader/src/test/shell/makeTestJars.sh index 7dfa64f..815cf28 100755 --- a/modules/local-caching-classloader/src/test/shell/makeTestJars.sh +++ b/modules/local-caching-classloader/src/test/shell/makeTestJars.sh @@ -26,7 +26,7 @@ fi for x in A B C D; do mkdir -p target/generated-sources/$x/test target/test-classes/ClassLoaderTest$x sed "s/XXX/$x/" target/generated-sources/$x/test/TestObject$x.java - "$JAVA_HOME/bin/javac" --source 11 --target 11 -cp target/test-classes target/generated-sources/$x/test/TestObject$x.java -d target/generated-sources/$x + "$JAVA_HOME/bin/javac" --release 11 -cp target/test-classes target/generated-sources/$x/test/TestObject$x.java -d target/generated-sources/$x "$JAVA_HOME/bin/jar" -cf target/test-classes/ClassLoaderTest$x/Test$x.jar -C target/generated-sources/$x test/TestObject$x.class rm -r target/generated-sources/$x done diff --git a/pom.xml b/pom.xml index 0c555a0..7eb8c83 100644 --- a/pom.xml +++ b/pom.xml @@ -327,7 +327,6 @@ under the License.]]> - From 5890ecf202e12e895a25f49c9c891fbe5356aa6a Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Mon, 17 Nov 2025 19:09:04 +0000 Subject: [PATCH 10/31] Fix javadoc --- .../lcc/LocalCachingContextClassLoaderFactory.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 538b213..b3cc4bd 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 @@ -32,6 +32,7 @@ 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.resolvers.FileResolver; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; import org.slf4j.Logger; @@ -40,23 +41,23 @@ /** * A ContextClassLoaderFactory implementation that creates and maintains a ClassLoader for a named * context. This factory expects the parameter passed to {@link #getClassLoader(String)} to be the - * URL of a json formatted {@link #ContextDefinition} file. The file contains an interval at which + * URL of a json formatted {@link ContextDefinition} file. The file contains an interval at which * this class should monitor the file for changes and a list of {@link Resource} objects. Each * resource is defined by a URL to the file and an expected MD5 hash value. - *

+ *

* The URLs supplied for the context definition file and for the resources can use one of the * following protocols: file://, http://, or hdfs://. - *

+ *

* As this class processes the ContextDefinition it fetches the contents of the resource from the * resource URL and caches it in a directory on the local filesystem. This class uses the value of * the system property {@link Constants#CACHE_DIR_PROPERTY} as the root directory and creates a * sub-directory for each context name. Each context cache directory contains a lock file and a copy * of each fetched resource that is named using the following format: fileName_checksum. - *

+ *

* The lock file prevents processes from manipulating the contexts of the context cache directory * concurrently, which enables the cache directories to be shared among multiple processes on the * host. - *

+ *

* Note that 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 From c5bd4ffed653be6bcd1ce472af6ba2da8bcb9b30 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 19 Nov 2025 16:54:11 +0000 Subject: [PATCH 11/31] Updated PR with the following changes: - Made some classes final for the spotbugs CT_CONSTRUCTOR_THROW error - Added retry with backoff to cacheResource - Replaced ConcurrentHashMap with Caffeine Cache - Renamed LocalCachingContextClassLoader to LocalCachingContext --- modules/local-caching-classloader/README.md | 6 +- modules/local-caching-classloader/pom.xml | 5 ++ ...ssLoader.java => LocalCachingContext.java} | 87 +++++++++---------- ...LocalCachingContextClassLoaderFactory.java | 78 +++++++---------- .../classloader/lcc/cache/CacheUtils.java | 9 +- .../lcc/resolvers/HdfsFileResolver.java | 11 +-- .../lcc/resolvers/LocalFileResolver.java | 11 +-- .../LocalCachingContextClassLoaderTest.java | 6 +- 8 files changed, 90 insertions(+), 123 deletions(-) rename modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/{LocalCachingContextClassLoader.java => LocalCachingContext.java} (80%) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index fc8f0d4..fcd36ca 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -18,12 +18,12 @@ limitations under the License. # Local Caching ClassLoader The LocalCachingContextClassLoaderFactory is an Accumulo ContextClassLoaderFactory implementation that creates and maintains a -LocalCachingContextClassLoader. The `LocalCachingContextClassLoaderFactory.getClassLoader(String)` method expects the method +LocalCachingContext. The `LocalCachingContextClassLoaderFactory.getClassLoader(String)` method expects the method argument to be a valid `file`, `hdfs`, `http` or `https` URL to a context definition file. The context definition file is a JSON formatted file that contains the name of the context, the interval in seconds at which the context definition file should be monitored, and a list of classpath resources. The LocalCachingContextClassLoaderFactory -creates the LocalCachingContextClassLoader based on the initial contents of the context definition file, and updates the classloader +creates the LocalCachingContext based on the initial contents of the context definition file, and updates the classloader as changes are noticed based on the monitoring interval. An example of the context definition file is below. ``` @@ -48,7 +48,7 @@ as changes are noticed based on the monitoring interval. An example of the conte ``` The system property `accumulo.classloader.cache.dir` is required to be set to a local directory on the host. The -LocalCachingContextClassLoader creates a directory at this location for each named context. Each context cache directory +LocalCachingContext creates a directory at this location for each named context. Each context cache directory contains a lock file and a copy of each fetched resource that is named in the context definition file using the format: `fileName_checksum`. The lock file is used with Java's `FileChannel.tryLock` to enable exclusive access (on supported platforms) to the directory from different processes on the same host. diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index 321ed89..a84afd2 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -51,6 +51,11 @@ spotbugs-annotations true + + com.github.ben-manes.caffeine + caffeine + provided + com.google.code.gson gson diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContext.java similarity index 80% rename from modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java rename to modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContext.java index be0d037..37d740c 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoader.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContext.java @@ -19,11 +19,13 @@ package org.apache.accumulo.classloader.lcc; import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; @@ -31,6 +33,7 @@ import java.util.Iterator; import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -40,10 +43,12 @@ import org.apache.accumulo.classloader.lcc.definition.Resource; import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.accumulo.core.util.Retry; +import org.apache.accumulo.core.util.Retry.RetryFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class LocalCachingContextClassLoader { +public final class LocalCachingContext { private static class ClassPathElement { private final FileResolver resolver; @@ -59,20 +64,10 @@ public ClassPathElement(FileResolver resolver, URL localCachedCopy, Objects.requireNonNull(localCachedCopyDigest, "local cached copy md5 must be supplied"); } - @SuppressWarnings("unused") - public FileResolver getResolver() { - return resolver; - } - public URL getLocalCachedCopyLocation() { return localCachedCopyLocation; } - @SuppressWarnings("unused") - public String getLocalCachedCopyDigest() { - return localCachedCopyDigest; - } - @Override public int hashCode() { return Objects.hash(localCachedCopyDigest, localCachedCopyLocation, resolver); @@ -101,7 +96,7 @@ public String toString() { } } - private static final Logger LOG = LoggerFactory.getLogger(LocalCachingContextClassLoader.class); + private static final Logger LOG = LoggerFactory.getLogger(LocalCachingContext.class); private final Path contextCacheDir; private final String contextName; @@ -109,37 +104,42 @@ public String toString() { private final AtomicBoolean elementsChanged = new AtomicBoolean(true); private final AtomicReference classloader = new AtomicReference<>(); private final AtomicReference definition = new AtomicReference<>(); + private final RetryFactory retryFactory = Retry.builder().infiniteRetries() + .retryAfter(1, TimeUnit.SECONDS).incrementBy(1, TimeUnit.SECONDS).maxWait(5, TimeUnit.MINUTES) + .backOffFactor(2).logInterval(1, TimeUnit.SECONDS).createFactory(); - public LocalCachingContextClassLoader(ContextDefinition contextDefinition) - throws ContextClassLoaderException { + public LocalCachingContext(ContextDefinition contextDefinition) + throws IOException, ContextClassLoaderException { this.definition.set(Objects.requireNonNull(contextDefinition, "definition must be supplied")); this.contextName = this.definition.get().getContextName(); this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(contextName); } - @Deprecated - @Override - protected final void finalize() { - /* - * unused; this is final due to finalizer attacks since the constructor throws exceptions (see - * spotbugs CT_CONSTRUCTOR_THROW) - */ - } - public ContextDefinition getDefinition() { return definition.get(); } private ClassPathElement cacheResource(final Resource resource) throws Exception { - final FileResolver source = FileResolver.resolve(resource.getURL()); final Path cacheLocation = contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum()); final File cacheFile = cacheLocation.toFile(); if (!Files.exists(cacheLocation)) { - LOG.trace("Caching resource {} at {}", source.getURL(), cacheFile.getAbsolutePath()); - try (InputStream is = source.getInputStream()) { - Files.copy(is, cacheLocation); + Retry retry = retryFactory.createRetry(); + boolean successful = false; + while (!successful) { + LOG.trace("Caching resource {} at {}", source.getURL(), cacheFile.getAbsolutePath()); + try (InputStream is = source.getInputStream()) { + Files.copy(is, cacheLocation, StandardCopyOption.REPLACE_EXISTING); + successful = true; + retry.logCompletion(LOG, "Resource " + source.getURL() + " cached locally"); + } catch (IOException e) { + LOG.error("Error copying resource from {}. Retrying...", source.getURL(), e); + retry.logRetry(LOG, "Unable to cache resource " + source.getURL()); + retry.waitForNextAttempt(LOG, "Cache resource " + source.getURL()); + } finally { + retry.useRetry(); + } } final String checksum = Constants.getChecksummer().digestAsHex(cacheFile); if (!resource.getChecksum().equals(checksum)) { @@ -153,9 +153,18 @@ private ClassPathElement cacheResource(final Resource resource) throws Exception // File exists, return new ClassPathElement based on existing file LOG.trace("Resource {} is already cached at {}", source.getURL(), cacheFile.getAbsolutePath()); - String fileName = cacheFile.getName(); - String[] parts = fileName.split("_"); - return new ClassPathElement(source, cacheFile.toURI().toURL(), parts[1]); + return new ClassPathElement(source, cacheFile.toURI().toURL(), resource.getChecksum()); + } + } + + private void cacheResources(final ContextDefinition def) throws Exception { + synchronized (elements) { + for (Resource updatedResource : def.getResources()) { + ClassPathElement cpe = cacheResource(updatedResource); + elements.add(cpe); + LOG.trace("Added element {} to classpath", cpe); + } + elementsChanged.set(true); } } @@ -168,10 +177,7 @@ public void initialize() { return; } try { - for (Resource r : definition.get().getResources()) { - ClassPathElement cpe = cacheResource(r); - addElement(cpe); - } + cacheResources(definition.get()); } finally { lockInfo.unlock(); } @@ -195,10 +201,7 @@ public void update(final ContextDefinition update) { } try { elements.clear(); - for (Resource updatedResource : update.getResources()) { - ClassPathElement cpe = cacheResource(updatedResource); - addElement(cpe); - } + cacheResources(update); this.definition.set(update); } finally { lockInfo.unlock(); @@ -209,14 +212,6 @@ public void update(final ContextDefinition update) { } } - private void addElement(ClassPathElement element) { - synchronized (elements) { - elements.add(element); - elementsChanged.set(true); - LOG.trace("Added element {} to classpath", element); - } - } - public ClassLoader getClassloader() { synchronized (elements) { if (classloader.get() == null || elementsChanged.get()) { 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 b3cc4bd..1987989 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 @@ -23,12 +23,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.lang.ref.SoftReference; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.accumulo.classloader.lcc.cache.CacheUtils; import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; @@ -38,6 +37,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + /** * A ContextClassLoaderFactory implementation that creates and maintains a ClassLoader for a named * context. This factory expects the parameter passed to {@link #getClassLoader(String)} to be the @@ -68,8 +70,8 @@ public class LocalCachingContextClassLoaderFactory implements ContextClassLoader private static final Logger LOG = LoggerFactory.getLogger(LocalCachingContextClassLoaderFactory.class); - private final ConcurrentHashMap> contexts = - new ConcurrentHashMap<>(); + private final Cache contexts = + Caffeine.newBuilder().weakValues().build(); private ContextDefinition parseContextDefinition(URL url) throws ContextClassLoaderException { LOG.trace("Retrieving context definition file from {}", url); @@ -90,19 +92,13 @@ private ContextDefinition parseContextDefinition(URL url) throws ContextClassLoa } } - private void monitorContext(final String contextLocation, boolean initialCall, int interval) { - final SoftReference ccl = contexts.get(contextLocation); - if (!initialCall && ccl == null) { - // context has been removed from the map, no need to check for update - return; - } - if (!initialCall && ccl.get() == null) { - // classloader has been garbage collected. Remove from the map and return - contexts.remove(contextLocation); - return; - } + private void monitorContext(final String contextLocation, int interval) { Constants.EXECUTOR.schedule(() -> { - final LocalCachingContextClassLoader classLoader = contexts.get(contextLocation).get(); + final LocalCachingContext classLoader = contexts.getIfPresent(contextLocation); + if (classLoader == null) { + // context has been removed from the map, no need to check for update + return; + } final ContextDefinition currentDef = classLoader.getDefinition(); try { final URL contextManifest = new URL(contextLocation); @@ -113,7 +109,7 @@ private void monitorContext(final String contextLocation, boolean initialCall, i } else { LOG.debug("Context definition for {} has not changed", currentDef.getContextName()); } - monitorContext(contextLocation, false, update.getMonitorIntervalSeconds()); + monitorContext(contextLocation, update.getMonitorIntervalSeconds()); } catch (Exception e) { LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", contextLocation, e); @@ -127,18 +123,27 @@ private void monitorContext(final String contextLocation, boolean initialCall, i public ClassLoader getClassLoader(final String contextLocation) throws ContextClassLoaderException { try { - SoftReference ccl = - contexts.computeIfAbsent(contextLocation, cn -> { - return new SoftReference<>(createContextClassLoader(contextLocation)); - }); - final LocalCachingContextClassLoader lcccl = ccl.get(); - if (ccl.get() == null) { - // SoftReference is in the map, but the classloader has been garbage collected. - // Need to recreate it - contexts.remove(contextLocation); - return getClassLoader(contextLocation); + final URL contextLocationUrl = new URL(contextLocation); + final AtomicBoolean newlyCreated = new AtomicBoolean(false); + final LocalCachingContext ccl = contexts.get(contextLocation, cn -> { + try { + CacheUtils.createBaseCacheDir(); + ContextDefinition def = parseContextDefinition(contextLocationUrl); + LocalCachingContext newCcl = new LocalCachingContext(def); + newCcl.initialize(); + newlyCreated.set(true); + return newCcl; + } catch (ContextClassLoaderException | IOException e) { + throw new RuntimeException("Error creating context classloader", e); + } + }); + if (newlyCreated.get()) { + monitorContext(contextLocation, ccl.getDefinition().getMonitorIntervalSeconds()); } - return lcccl.getClassloader(); + return ccl.getClassloader(); + } catch (MalformedURLException e) { + throw new ContextClassLoaderException( + "Expected valid URL to context definition file but received: " + contextLocation, e); } catch (RuntimeException re) { Throwable t = re.getCause(); if (t != null && t instanceof ContextClassLoaderException) { @@ -149,21 +154,4 @@ public ClassLoader getClassLoader(final String contextLocation) } } - private LocalCachingContextClassLoader createContextClassLoader(final String contextLocation) { - try { - URL contextManifest = new URL(contextLocation); - CacheUtils.createBaseCacheDir(); - ContextDefinition m = parseContextDefinition(contextManifest); - LocalCachingContextClassLoader newCcl = new LocalCachingContextClassLoader(m); - newCcl.initialize(); - monitorContext(contextLocation, true, m.getMonitorIntervalSeconds()); - return newCcl; - } catch (MalformedURLException e) { - throw new RuntimeException( - "Expected valid URL to context definition file but received: " + contextLocation, e); - } catch (ContextClassLoaderException e) { - throw new RuntimeException("Error processing context definition", e); - } - } - } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index f4a5fd6..49905ff 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -80,18 +80,15 @@ public void unlock() throws IOException { } - private static Path mkdir(Path p) throws ContextClassLoaderException { + private static Path mkdir(Path p) throws IOException { try { return Files.createDirectory(p, PERMISSIONS); } catch (FileAlreadyExistsException e) { return p; - } catch (IOException e) { - throw new ContextClassLoaderException( - "Error creating cache directory: " + p.toFile().getAbsolutePath(), e); } } - public static Path createBaseCacheDir() throws ContextClassLoaderException { + public static Path createBaseCacheDir() throws IOException, ContextClassLoaderException { final String prop = Constants.CACHE_DIR_PROPERTY; final String cacheDir = System.getProperty(prop); if (cacheDir == null) { @@ -101,7 +98,7 @@ public static Path createBaseCacheDir() throws ContextClassLoaderException { } public static Path createOrGetContextCacheDir(String contextName) - throws ContextClassLoaderException { + throws IOException, ContextClassLoaderException { Path baseContextDir = createBaseCacheDir(); return mkdir(baseContextDir.resolve(contextName)); } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java index a2b59cb..808d5b3 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java @@ -29,7 +29,7 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; -public class HdfsFileResolver extends FileResolver { +public final class HdfsFileResolver extends FileResolver { private final Configuration hadoopConf = new Configuration(); private final FileSystem fs; @@ -51,15 +51,6 @@ protected HdfsFileResolver(URL url) throws ContextClassLoaderException { } } - @Deprecated - @Override - protected final void finalize() { - /* - * unused; this is final due to finalizer attacks since the constructor throws exceptions (see - * spotbugs CT_CONSTRUCTOR_THROW) - */ - } - @Override public String getFileName() throws URISyntaxException { return this.path.getName(); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java index c3d0e57..6aa18fe 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java @@ -31,7 +31,7 @@ import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; import org.apache.hadoop.shaded.org.apache.commons.io.FileUtils; -public class LocalFileResolver extends FileResolver { +public final class LocalFileResolver extends FileResolver { private final File file; @@ -53,15 +53,6 @@ public LocalFileResolver(URL url) throws ContextClassLoaderException { } } - @Deprecated - @Override - protected final void finalize() { - /* - * unused; this is final due to finalizer attacks since the constructor throws exceptions (see - * spotbugs CT_CONSTRUCTOR_THROW) - */ - } - @Override public String getFileName() throws URISyntaxException { Path filePath = Paths.get(getURL().toURI()).getFileName(); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java index 8db23f9..2a4173a 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java @@ -108,7 +108,7 @@ public static void afterAll() throws Exception { @Test public void testInitialize() throws ContextClassLoaderException, IOException { - LocalCachingContextClassLoader lcccl = new LocalCachingContextClassLoader(def); + LocalCachingContext lcccl = new LocalCachingContext(def); lcccl.initialize(); // Confirm the 3 jars are cached locally @@ -126,7 +126,7 @@ public void testInitialize() throws ContextClassLoaderException, IOException { @Test public void testClassLoader() throws Exception { - LocalCachingContextClassLoader lcccl = new LocalCachingContextClassLoader(def); + LocalCachingContext lcccl = new LocalCachingContext(def); lcccl.initialize(); ClassLoader contextClassLoader = lcccl.getClassloader(); @@ -150,7 +150,7 @@ public void testClassLoader() throws Exception { @Test public void testUpdate() throws Exception { - LocalCachingContextClassLoader lcccl = new LocalCachingContextClassLoader(def); + LocalCachingContext lcccl = new LocalCachingContext(def); lcccl.initialize(); final ClassLoader contextClassLoader = lcccl.getClassloader(); From 6cb1ee2e27e4e67a86548ffa4a0e4981eecfd1a6 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 19 Nov 2025 18:10:55 +0000 Subject: [PATCH 12/31] minor test changes, warn when context name changed in an update --- .../lcc/LocalCachingContextClassLoaderFactory.java | 5 +++++ .../lcc/LocalCachingContextClassLoaderFactoryTest.java | 8 ++++---- ...assLoaderTest.java => LocalCachingContextTest.java} | 10 +++++----- 3 files changed, 14 insertions(+), 9 deletions(-) rename modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/{LocalCachingContextClassLoaderTest.java => LocalCachingContextTest.java} (95%) 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 1987989..5813a7a 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 @@ -105,6 +105,11 @@ private void monitorContext(final String contextLocation, int interval) { final ContextDefinition update = parseContextDefinition(contextManifest); if (!Arrays.equals(currentDef.getChecksum(), update.getChecksum())) { LOG.debug("Context definition for {} has changed", currentDef.getContextName()); + if (!currentDef.getContextName().equals(update.getContextName())) { + LOG.warn( + "Context name changed for context {}, but context cache directory will remain {}", + contextLocation, currentDef.getContextName()); + } classLoader.update(update); } else { LOG.debug("Context definition for {} has not changed", currentDef.getContextName()); 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 1ee1884..015cc88 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 @@ -73,16 +73,16 @@ public static void beforeAll() throws Exception { // Find the Test jar files jarAOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestA/TestA.jar"); + LocalCachingContextClassLoaderFactoryTest.class.getResource("/ClassLoaderTestA/TestA.jar"); assertNotNull(jarAOrigLocation); jarBOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestB/TestB.jar"); + LocalCachingContextClassLoaderFactoryTest.class.getResource("/ClassLoaderTestB/TestB.jar"); assertNotNull(jarBOrigLocation); jarCOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestC/TestC.jar"); + LocalCachingContextClassLoaderFactoryTest.class.getResource("/ClassLoaderTestC/TestC.jar"); assertNotNull(jarCOrigLocation); jarDOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestD/TestD.jar"); + LocalCachingContextClassLoaderFactoryTest.class.getResource("/ClassLoaderTestD/TestD.jar"); assertNotNull(jarDOrigLocation); // Put B into HDFS diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java similarity index 95% rename from modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java rename to modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java index 2a4173a..67327e1 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java @@ -45,7 +45,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -public class LocalCachingContextClassLoaderTest { +public class LocalCachingContextTest { private static final String CONTEXT_NAME = "TEST_CONTEXT"; private static final int MONITOR_INTERVAL_SECS = 5; @@ -63,13 +63,13 @@ public static void beforeAll() throws Exception { // Find the Test jar files final URL jarAOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestA/TestA.jar"); + LocalCachingContextTest.class.getResource("/ClassLoaderTestA/TestA.jar"); assertNotNull(jarAOrigLocation); final URL jarBOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestB/TestB.jar"); + LocalCachingContextTest.class.getResource("/ClassLoaderTestB/TestB.jar"); assertNotNull(jarBOrigLocation); final URL jarCOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestC/TestC.jar"); + LocalCachingContextTest.class.getResource("/ClassLoaderTestC/TestC.jar"); assertNotNull(jarCOrigLocation); // Put B into HDFS @@ -176,7 +176,7 @@ public void testUpdate() throws Exception { // Add D final URL jarDOrigLocation = - LocalCachingContextClassLoaderTest.class.getResource("/ClassLoaderTestD/TestD.jar"); + LocalCachingContextTest.class.getResource("/ClassLoaderTestD/TestD.jar"); assertNotNull(jarDOrigLocation); updatedResources.add(new Resource(jarDOrigLocation.toString(), TestUtils.computeResourceChecksum(jarDOrigLocation))); From d4e39bb6a94c2a98ef40f91de732285b846062c7 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 19 Nov 2025 18:39:16 +0000 Subject: [PATCH 13/31] Addressed some PR suggestions --- .../classloader/lcc/LocalCachingContext.java | 57 +++++++++++-------- ...LocalCachingContextClassLoaderFactory.java | 4 +- 2 files changed, 34 insertions(+), 27 deletions(-) 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 37d740c..8662dc1 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 @@ -18,6 +18,9 @@ */ package org.apache.accumulo.classloader.lcc; +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -25,7 +28,6 @@ import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; @@ -34,7 +36,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.apache.accumulo.classloader.lcc.cache.CacheUtils; @@ -101,7 +102,6 @@ public String toString() { private final Path contextCacheDir; private final String contextName; private final Set elements = new HashSet<>(); - private final AtomicBoolean elementsChanged = new AtomicBoolean(true); private final AtomicReference classloader = new AtomicReference<>(); private final AtomicReference definition = new AtomicReference<>(); private final RetryFactory retryFactory = Retry.builder().infiniteRetries() @@ -121,18 +121,22 @@ public ContextDefinition getDefinition() { private ClassPathElement cacheResource(final Resource resource) throws Exception { final FileResolver source = FileResolver.resolve(resource.getURL()); - final Path cacheLocation = + final Path tmpCacheLocation = + contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum() + "_tmp"); + final Path finalCacheLocation = contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum()); - final File cacheFile = cacheLocation.toFile(); - if (!Files.exists(cacheLocation)) { + final File cacheFile = finalCacheLocation.toFile(); + if (!Files.exists(finalCacheLocation)) { Retry retry = retryFactory.createRetry(); boolean successful = false; while (!successful) { LOG.trace("Caching resource {} at {}", source.getURL(), cacheFile.getAbsolutePath()); try (InputStream is = source.getInputStream()) { - Files.copy(is, cacheLocation, StandardCopyOption.REPLACE_EXISTING); + Files.copy(is, tmpCacheLocation, REPLACE_EXISTING); + Files.move(tmpCacheLocation, finalCacheLocation, ATOMIC_MOVE); successful = true; - retry.logCompletion(LOG, "Resource " + source.getURL() + " cached locally"); + retry.logCompletion(LOG, + "Resource " + source.getURL() + " cached locally as " + finalCacheLocation); } catch (IOException e) { LOG.error("Error copying resource from {}. Retrying...", source.getURL(), e); retry.logRetry(LOG, "Unable to cache resource " + source.getURL()); @@ -164,7 +168,7 @@ private void cacheResources(final ContextDefinition def) throws Exception { elements.add(cpe); LOG.trace("Added element {} to classpath", cpe); } - elementsChanged.set(true); + classloader.set(null); } } @@ -213,24 +217,27 @@ public void update(final ContextDefinition update) { } public ClassLoader getClassloader() { + + ClassLoader currentCL = classloader.get(); + if (currentCL != null) { + return currentCL; + } + synchronized (elements) { - if (classloader.get() == null || elementsChanged.get()) { - LOG.trace("Class path contents have changed, creating new classloader"); - URL[] urls = new URL[elements.size()]; - Iterator iter = elements.iterator(); - for (int x = 0; x < elements.size(); x++) { - urls[x] = iter.next().getLocalCachedCopyLocation(); - } - elementsChanged.set(false); - final URLClassLoader cl = - AccessController.doPrivileged((PrivilegedAction) () -> { - return new URLClassLoader(contextName, urls, this.getClass().getClassLoader()); - }); - classloader.set(cl); - LOG.trace("New classloader created from URLs: {}", - Arrays.asList(classloader.get().getURLs())); + LOG.trace("Class path contents have changed, creating new classloader"); + URL[] urls = new URL[elements.size()]; + Iterator iter = elements.iterator(); + for (int x = 0; x < elements.size(); x++) { + urls[x] = iter.next().getLocalCachedCopyLocation(); } + final URLClassLoader cl = + AccessController.doPrivileged((PrivilegedAction) () -> { + return new URLClassLoader(contextName, urls, this.getClass().getClassLoader()); + }); + classloader.set(cl); + LOG.trace("New classloader created from URLs: {}", + Arrays.asList(classloader.get().getURLs())); + return cl; } - return classloader.get(); } } 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 5813a7a..1f96d42 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 @@ -104,7 +104,7 @@ private void monitorContext(final String contextLocation, int interval) { final URL contextManifest = new URL(contextLocation); final ContextDefinition update = parseContextDefinition(contextManifest); if (!Arrays.equals(currentDef.getChecksum(), update.getChecksum())) { - LOG.debug("Context definition for {} has changed", currentDef.getContextName()); + LOG.debug("Context definition for {} has changed", contextLocation); if (!currentDef.getContextName().equals(update.getContextName())) { LOG.warn( "Context name changed for context {}, but context cache directory will remain {}", @@ -112,7 +112,7 @@ private void monitorContext(final String contextLocation, int interval) { } classLoader.update(update); } else { - LOG.debug("Context definition for {} has not changed", currentDef.getContextName()); + LOG.debug("Context definition for {} has not changed", contextLocation); } monitorContext(contextLocation, update.getMonitorIntervalSeconds()); } catch (Exception e) { From 58932fb0a04fce5328594181b7720ed72e05bdcd Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 19 Nov 2025 19:09:02 +0000 Subject: [PATCH 14/31] Modified initialize and update methods to wait for lock --- .../classloader/lcc/LocalCachingContext.java | 49 ++++++++++++------- ...LocalCachingContextClassLoaderFactory.java | 11 ++++- .../lcc/LocalCachingContextTest.java | 4 +- 3 files changed, 41 insertions(+), 23 deletions(-) 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 8662dc1..8529b7b 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 @@ -24,6 +24,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; @@ -119,7 +120,8 @@ public ContextDefinition getDefinition() { return definition.get(); } - private ClassPathElement cacheResource(final Resource resource) throws Exception { + private ClassPathElement cacheResource(final Resource resource) + throws InterruptedException, IOException, ContextClassLoaderException, URISyntaxException { final FileResolver source = FileResolver.resolve(resource.getURL()); final Path tmpCacheLocation = contextCacheDir.resolve(source.getFileName() + "_" + resource.getChecksum() + "_tmp"); @@ -161,7 +163,8 @@ private ClassPathElement cacheResource(final Resource resource) throws Exception } } - private void cacheResources(final ContextDefinition def) throws Exception { + private void cacheResources(final ContextDefinition def) + throws InterruptedException, IOException, ContextClassLoaderException, URISyntaxException { synchronized (elements) { for (Resource updatedResource : def.getResources()) { ClassPathElement cpe = cacheResource(updatedResource); @@ -172,14 +175,18 @@ private void cacheResources(final ContextDefinition def) throws Exception { } } - public void initialize() { + public void initialize() + throws InterruptedException, IOException, ContextClassLoaderException, URISyntaxException { try { + LockInfo lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); + while (lockInfo == null) { + // something else is updating this directory + LOG.info("Directory {} locked, another process must be updating the class loader contents. " + + "Retrying in 1 second", contextCacheDir); + Thread.sleep(1000); + lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); + } synchronized (elements) { - final LockInfo lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); - if (lockInfo == null) { - // something else is updating this directory - return; - } try { cacheResources(definition.get()); } finally { @@ -188,21 +195,26 @@ public void initialize() { } } catch (Exception e) { LOG.error("Error initializing context: " + contextName, e); + throw e; } } - public void update(final ContextDefinition update) { + public void update(final ContextDefinition update) + throws InterruptedException, IOException, ContextClassLoaderException, URISyntaxException { Objects.requireNonNull(update, "definition must be supplied"); if (definition.get().getResources().equals(update.getResources())) { return; } - synchronized (elements) { - try { - final LockInfo lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); - if (lockInfo == null) { - // something else is updating this directory - return; - } + try { + LockInfo lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); + while (lockInfo == null) { + // something else is updating this directory + LOG.info("Directory {} locked, another process must be updating the class loader contents. " + + "Retrying in 1 second", contextCacheDir); + Thread.sleep(1000); + lockInfo = CacheUtils.lockContextCacheDir(contextCacheDir); + } + synchronized (elements) { try { elements.clear(); cacheResources(update); @@ -210,9 +222,10 @@ public void update(final ContextDefinition update) { } finally { lockInfo.unlock(); } - } catch (Exception e) { - LOG.error("Error updating context: " + contextName, e); } + } catch (Exception e) { + LOG.error("Error updating context: " + contextName, e); + throw e; } } 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 1f96d42..9cb0554 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,7 +24,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -115,7 +117,8 @@ private void monitorContext(final String contextLocation, int interval) { LOG.debug("Context definition for {} has not changed", contextLocation); } monitorContext(contextLocation, update.getMonitorIntervalSeconds()); - } catch (Exception e) { + } catch (ContextClassLoaderException | InterruptedException | IOException + | NoSuchAlgorithmException | URISyntaxException e) { LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", contextLocation, e); } @@ -138,7 +141,8 @@ public ClassLoader getClassLoader(final String contextLocation) newCcl.initialize(); newlyCreated.set(true); return newCcl; - } catch (ContextClassLoaderException | IOException e) { + } catch (ContextClassLoaderException | InterruptedException | IOException + | URISyntaxException e) { throw new RuntimeException("Error creating context classloader", e); } }); @@ -151,6 +155,9 @@ public ClassLoader getClassLoader(final String contextLocation) "Expected valid URL to context definition file but received: " + contextLocation, e); } catch (RuntimeException re) { Throwable t = re.getCause(); + if (t != null && t instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } if (t != null && t instanceof ContextClassLoaderException) { throw (ContextClassLoaderException) t; } else { diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java index 67327e1..877cfd4 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java @@ -25,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; @@ -34,7 +33,6 @@ import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.classloader.lcc.definition.Resource; -import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; import org.apache.hadoop.fs.Path; @@ -107,7 +105,7 @@ public static void afterAll() throws Exception { } @Test - public void testInitialize() throws ContextClassLoaderException, IOException { + public void testInitialize() throws Exception { LocalCachingContext lcccl = new LocalCachingContext(def); lcccl.initialize(); From bafd0bd09eba81eb16a9691fc7e4e74a3cb75ff3 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 19 Nov 2025 21:02:24 +0000 Subject: [PATCH 15/31] minor cleanup, fixup imports, added some javadoc --- .../classloader/lcc/LocalCachingContext.java | 20 +++++++++++----- ...LocalCachingContextClassLoaderFactory.java | 21 +++++++++++++---- .../classloader/lcc/cache/CacheUtils.java | 23 +++++++++++-------- .../lcc/resolvers/FileResolver.java | 19 ++++++++------- .../lcc/resolvers/HdfsFileResolver.java | 10 +++----- .../lcc/resolvers/HttpFileResolver.java | 15 ++++-------- .../lcc/resolvers/LocalFileResolver.java | 17 ++++---------- 7 files changed, 68 insertions(+), 57 deletions(-) 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 8529b7b..484f8c8 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 @@ -20,6 +20,8 @@ import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.Objects.hash; +import static java.util.Objects.requireNonNull; import java.io.File; import java.io.IOException; @@ -59,11 +61,11 @@ private static class ClassPathElement { public ClassPathElement(FileResolver resolver, URL localCachedCopy, String localCachedCopyDigest) { - this.resolver = Objects.requireNonNull(resolver, "resolver must be supplied"); + this.resolver = requireNonNull(resolver, "resolver must be supplied"); this.localCachedCopyLocation = - Objects.requireNonNull(localCachedCopy, "local cached copy location must be supplied"); + requireNonNull(localCachedCopy, "local cached copy location must be supplied"); this.localCachedCopyDigest = - Objects.requireNonNull(localCachedCopyDigest, "local cached copy md5 must be supplied"); + requireNonNull(localCachedCopyDigest, "local cached copy md5 must be supplied"); } public URL getLocalCachedCopyLocation() { @@ -72,7 +74,7 @@ public URL getLocalCachedCopyLocation() { @Override public int hashCode() { - return Objects.hash(localCachedCopyDigest, localCachedCopyLocation, resolver); + return hash(localCachedCopyDigest, localCachedCopyLocation, resolver); } @Override @@ -111,7 +113,7 @@ public String toString() { public LocalCachingContext(ContextDefinition contextDefinition) throws IOException, ContextClassLoaderException { - this.definition.set(Objects.requireNonNull(contextDefinition, "definition must be supplied")); + this.definition.set(requireNonNull(contextDefinition, "definition must be supplied")); this.contextName = this.definition.get().getContextName(); this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(contextName); } @@ -201,7 +203,7 @@ public void initialize() public void update(final ContextDefinition update) throws InterruptedException, IOException, ContextClassLoaderException, URISyntaxException { - Objects.requireNonNull(update, "definition must be supplied"); + requireNonNull(update, "definition must be supplied"); if (definition.get().getResources().equals(update.getResources())) { return; } @@ -237,6 +239,12 @@ public ClassLoader getClassloader() { } synchronized (elements) { + + currentCL = classloader.get(); + if (currentCL != null) { + return currentCL; + } + LOG.trace("Class path contents have changed, creating new classloader"); URL[] urls = new URL[elements.size()]; Iterator iter = elements.iterator(); 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 9cb0554..ca42d94 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 @@ -19,6 +19,7 @@ package org.apache.accumulo.classloader.lcc; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; import java.io.IOException; import java.io.InputStream; @@ -30,6 +31,7 @@ import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.accumulo.classloader.lcc.cache.CacheUtils; import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; @@ -75,9 +77,10 @@ public class LocalCachingContextClassLoaderFactory implements ContextClassLoader private final Cache contexts = Caffeine.newBuilder().weakValues().build(); - private ContextDefinition parseContextDefinition(URL url) throws ContextClassLoaderException { + private ContextDefinition parseContextDefinition(final URL url) + throws ContextClassLoaderException { LOG.trace("Retrieving context definition file from {}", url); - FileResolver resolver = FileResolver.resolve(url); + final FileResolver resolver = FileResolver.resolve(url); try { try (InputStream is = resolver.getInputStream()) { ContextDefinition def = @@ -94,6 +97,11 @@ private ContextDefinition parseContextDefinition(URL url) throws ContextClassLoa } } + /** + * Schedule a task to execute at {@code interval} seconds to update the LocalCachingContext if the + * ContextDefinition has changed. The task schedules a follow-on task at the update interval value + * (if it changed). + */ private void monitorContext(final String contextLocation, int interval) { Constants.EXECUTOR.schedule(() -> { final LocalCachingContext classLoader = contexts.getIfPresent(contextLocation); @@ -101,10 +109,11 @@ private void monitorContext(final String contextLocation, int interval) { // context has been removed from the map, no need to check for update return; } + final AtomicInteger nextInterval = new AtomicInteger(interval); final ContextDefinition currentDef = classLoader.getDefinition(); try { - final URL contextManifest = new URL(contextLocation); - final ContextDefinition update = parseContextDefinition(contextManifest); + final URL contextLocationUrl = new URL(contextLocation); + final ContextDefinition update = parseContextDefinition(contextLocationUrl); if (!Arrays.equals(currentDef.getChecksum(), update.getChecksum())) { LOG.debug("Context definition for {} has changed", contextLocation); if (!currentDef.getContextName().equals(update.getContextName())) { @@ -113,6 +122,7 @@ private void monitorContext(final String contextLocation, int interval) { contextLocation, currentDef.getContextName()); } classLoader.update(update); + nextInterval.set(update.getMonitorIntervalSeconds()); } else { LOG.debug("Context definition for {} has not changed", contextLocation); } @@ -121,6 +131,8 @@ private void monitorContext(final String contextLocation, int interval) { | NoSuchAlgorithmException | URISyntaxException e) { LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", contextLocation, e); + } finally { + monitorContext(contextLocation, nextInterval.get()); } }, interval, TimeUnit.SECONDS); LOG.trace("Monitoring context definition file {} for changes at {} second intervals", @@ -130,6 +142,7 @@ private void monitorContext(final String contextLocation, int interval) { @Override public ClassLoader getClassLoader(final String contextLocation) throws ContextClassLoaderException { + requireNonNull(contextLocation, "context name must be supplied"); try { final URL contextLocationUrl = new URL(contextLocation); final AtomicBoolean newlyCreated = new AtomicBoolean(false); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index 49905ff..b1a3004 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -23,6 +23,7 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; +import static java.util.Objects.requireNonNull; import java.io.IOException; import java.net.URI; @@ -38,7 +39,6 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; -import java.util.Objects; import java.util.Set; import org.apache.accumulo.classloader.lcc.Constants; @@ -60,8 +60,8 @@ public static class LockInfo { private final FileLock lock; public LockInfo(FileChannel channel, FileLock lock) { - this.channel = Objects.requireNonNull(channel, "channel must be supplied"); - this.lock = Objects.requireNonNull(lock, "lock must be supplied"); + this.channel = requireNonNull(channel, "channel must be supplied"); + this.lock = requireNonNull(lock, "lock must be supplied"); } @SuppressFBWarnings(value = "EI_EXPOSE_REP") @@ -80,7 +80,7 @@ public void unlock() throws IOException { } - private static Path mkdir(Path p) throws IOException { + private static Path mkdir(final Path p) throws IOException { try { return Files.createDirectory(p, PERMISSIONS); } catch (FileAlreadyExistsException e) { @@ -97,20 +97,25 @@ public static Path createBaseCacheDir() throws IOException, ContextClassLoaderEx return mkdir(Paths.get(URI.create(cacheDir))); } - public static Path createOrGetContextCacheDir(String contextName) + public static Path createOrGetContextCacheDir(final String contextName) throws IOException, ContextClassLoaderException { Path baseContextDir = createBaseCacheDir(); return mkdir(baseContextDir.resolve(contextName)); } - public static LockInfo lockContextCacheDir(Path contextCacheDir) + /** + * Acquire an exclusive lock on the "lock_file" file in the context cache directory. Returns null + * if lock can not be acquired. Caller MUST call LockInfo.unlock when done manipulating the cache + * directory + */ + public static LockInfo lockContextCacheDir(final Path contextCacheDir) throws ContextClassLoaderException { - Path lockFilePath = contextCacheDir.resolve(lockFileName); + final Path lockFilePath = contextCacheDir.resolve(lockFileName); try { - FileChannel channel = FileChannel.open(lockFilePath, + final FileChannel channel = FileChannel.open(lockFilePath, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE), PERMISSIONS); try { - FileLock lock = channel.tryLock(); + final FileLock lock = channel.tryLock(); if (lock == null) { // something else has the lock channel.close(); diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java index 9c4aecb..3fcc261 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java @@ -18,8 +18,11 @@ */ package org.apache.accumulo.classloader.lcc.resolvers; +import static java.util.Objects.hash; +import static java.util.Objects.requireNonNull; + +import java.io.IOException; import java.io.InputStream; -import java.net.URISyntaxException; import java.net.URL; import java.util.Objects; @@ -28,8 +31,8 @@ public abstract class FileResolver { public static FileResolver resolve(URL url) throws ContextClassLoaderException { - String protocol = url.getProtocol(); - switch (protocol) { + requireNonNull(url, "URL must be supplied"); + switch (url.getProtocol()) { case "http": case "https": return new HttpFileResolver(url); @@ -38,11 +41,11 @@ public static FileResolver resolve(URL url) throws ContextClassLoaderException { case "hdfs": return new HdfsFileResolver(url); default: - throw new ContextClassLoaderException("Unhandled protocol: " + protocol); + throw new ContextClassLoaderException("Unhandled protocol: " + url.getProtocol()); } } - protected final URL url; + private final URL url; protected FileResolver(URL url) throws ContextClassLoaderException { this.url = url; @@ -52,13 +55,13 @@ public URL getURL() { return this.url; } - public abstract String getFileName() throws URISyntaxException; + public abstract String getFileName(); - public abstract InputStream getInputStream() throws ContextClassLoaderException; + public abstract InputStream getInputStream() throws IOException; @Override public int hashCode() { - return Objects.hash(url); + return hash(url); } @Override diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java index 808d5b3..f163ba1 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java @@ -52,16 +52,12 @@ protected HdfsFileResolver(URL url) throws ContextClassLoaderException { } @Override - public String getFileName() throws URISyntaxException { + public String getFileName() { return this.path.getName(); } @Override - public InputStream getInputStream() throws ContextClassLoaderException { - try { - return fs.open(path); - } catch (IOException e) { - throw new ContextClassLoaderException("Error opening file at url: " + url, e); - } + public InputStream getInputStream() throws IOException { + return fs.open(path); } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java index 1b491bb..76f8442 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java @@ -20,32 +20,27 @@ import java.io.IOException; import java.io.InputStream; -import java.net.URISyntaxException; import java.net.URL; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -public class HttpFileResolver extends FileResolver { +public final class HttpFileResolver extends FileResolver { protected HttpFileResolver(URL url) throws ContextClassLoaderException { super(url); } @Override - public String getFileName() throws URISyntaxException { - String path = this.url.getPath(); + public String getFileName() { + String path = getURL().getPath(); return path.substring(path.lastIndexOf("/") + 1); } @Override @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD") - public InputStream getInputStream() throws ContextClassLoaderException { - try { - return url.openStream(); - } catch (IOException e) { - throw new ContextClassLoaderException("Error opening file at url: " + url, e); - } + public InputStream getInputStream() throws IOException { + return getURL().openStream(); } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java index 6aa18fe..5aa10de 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java @@ -54,21 +54,12 @@ public LocalFileResolver(URL url) throws ContextClassLoaderException { } @Override - public String getFileName() throws URISyntaxException { - Path filePath = Paths.get(getURL().toURI()).getFileName(); - if (filePath != null) { - return filePath.toString(); - } else { - return null; - } + public String getFileName() { + return file.getName(); } @Override - public FileInputStream getInputStream() throws ContextClassLoaderException { - try { - return FileUtils.openInputStream(file); - } catch (IOException e) { - throw new ContextClassLoaderException("Error opening file at url: " + url, e); - } + public FileInputStream getInputStream() throws IOException { + return FileUtils.openInputStream(file); } } From c3adb171729a46366a5933278ddf9ce32484561c Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 19 Nov 2025 21:39:29 +0000 Subject: [PATCH 16/31] Modified cache from weakValues to expireAfterAccess(24 hours) --- .../classloader/lcc/LocalCachingContextClassLoaderFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ca42d94..b3c63c3 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 @@ -75,7 +75,7 @@ public class LocalCachingContextClassLoaderFactory implements ContextClassLoader LoggerFactory.getLogger(LocalCachingContextClassLoaderFactory.class); private final Cache contexts = - Caffeine.newBuilder().weakValues().build(); + Caffeine.newBuilder().expireAfterAccess(24, TimeUnit.HOURS).build(); private ContextDefinition parseContextDefinition(final URL url) throws ContextClassLoaderException { From f4387dd0fe1493a63819401ef8ea43e893ffa9c1 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 20 Nov 2025 19:53:03 +0000 Subject: [PATCH 17/31] Addressed PR suggestions, refactored tests for better readability --- .../classloader/lcc/LocalCachingContext.java | 4 +- ...LocalCachingContextClassLoaderFactory.java | 20 +- .../lcc/definition/ContextDefinition.java | 38 +- .../classloader/lcc/definition/Resource.java | 11 +- ...lCachingContextClassLoaderFactoryTest.java | 519 ++++++++++++------ .../lcc/LocalCachingContextTest.java | 93 ++-- .../accumulo/classloader/lcc/TestUtils.java | 77 ++- .../classloader/lcc/cache/CacheUtilsTest.java | 6 + .../src/test/resources/log4j2-test.properties | 2 +- 9 files changed, 512 insertions(+), 258 deletions(-) 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 484f8c8..0082bbc 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 @@ -151,8 +151,10 @@ private ClassPathElement cacheResource(final Resource resource) } final String checksum = Constants.getChecksummer().digestAsHex(cacheFile); if (!resource.getChecksum().equals(checksum)) { - LOG.error("Checksum {} for resource {} does not match checksum in context definition {}", + LOG.error( + "Checksum {} for resource {} does not match checksum in context definition {}, removing cached copy.", checksum, source.getURL(), resource.getChecksum()); + Files.delete(finalCacheLocation); throw new IllegalStateException("Checksum " + checksum + " for resource " + source.getURL() + " does not match checksum in context definition " + resource.getChecksum()); } 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 b3c63c3..bcb4e42 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 @@ -118,13 +118,14 @@ private void monitorContext(final String contextLocation, int interval) { LOG.debug("Context definition for {} has changed", contextLocation); if (!currentDef.getContextName().equals(update.getContextName())) { LOG.warn( - "Context name changed for context {}, but context cache directory will remain {}", - contextLocation, currentDef.getContextName()); + "Context name changed for context {}, but context cache directory will remain {} (old={}, new={})", + contextLocation, currentDef.getContextName(), currentDef.getContextName(), + update.getContextName()); } classLoader.update(update); nextInterval.set(update.getMonitorIntervalSeconds()); } else { - LOG.debug("Context definition for {} has not changed", contextLocation); + LOG.trace("Context definition for {} has not changed", contextLocation); } monitorContext(contextLocation, update.getMonitorIntervalSeconds()); } catch (ContextClassLoaderException | InterruptedException | IOException @@ -139,6 +140,14 @@ private void monitorContext(final String contextLocation, int interval) { contextLocation, interval); } + // for tests only + void resetForTests() { + // Removing the contexts will cause the + // background monitor task to end + contexts.invalidateAll(); + contexts.cleanUp(); + } + @Override public ClassLoader getClassLoader(final String contextLocation) throws ContextClassLoaderException { @@ -154,8 +163,7 @@ public ClassLoader getClassLoader(final String contextLocation) newCcl.initialize(); newlyCreated.set(true); return newCcl; - } catch (ContextClassLoaderException | InterruptedException | IOException - | URISyntaxException e) { + } catch (Exception e) { throw new RuntimeException("Error creating context classloader", e); } }); @@ -174,7 +182,7 @@ public ClassLoader getClassLoader(final String contextLocation) if (t != null && t instanceof ContextClassLoaderException) { throw (ContextClassLoaderException) t; } else { - throw new ContextClassLoaderException(re.getMessage(), re); + throw new ContextClassLoaderException(t.getMessage(), t); } } } 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 b1bb0e1..22d87e5 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 @@ -18,11 +18,19 @@ */ package org.apache.accumulo.classloader.lcc.definition; +import static java.util.Objects.hash; +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.security.NoSuchAlgorithmException; -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.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; import com.google.common.base.Preconditions; @@ -30,20 +38,34 @@ @SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) public class ContextDefinition { + + public static ContextDefinition create(String contextName, int monitorIntervalSecs, + URL... sources) throws ContextClassLoaderException, IOException { + TreeSet resources = new TreeSet<>(); + for (URL u : sources) { + FileResolver resolver = FileResolver.resolve(u); + try (InputStream is = resolver.getInputStream()) { + String checksum = Constants.getChecksummer().digestAsHex(is); + resources.add(new Resource(u.toString(), checksum)); + } + } + return new ContextDefinition(contextName, monitorIntervalSecs, resources); + } + private String contextName; private int monitorIntervalSeconds; - private List resources; + private TreeSet resources; private volatile transient byte[] checksum = null; public ContextDefinition() {} public ContextDefinition(String contextName, int monitorIntervalSeconds, - List resources) { - this.contextName = Objects.requireNonNull(contextName, "context name must be supplied"); + TreeSet resources) { + this.contextName = requireNonNull(contextName, "context name must be supplied"); Preconditions.checkArgument(monitorIntervalSeconds > 0, "monitor interval must be greater than zero"); this.monitorIntervalSeconds = monitorIntervalSeconds; - this.resources = Objects.requireNonNull(resources, "resources must be supplied"); + this.resources = requireNonNull(resources, "resources must be supplied"); } public String getContextName() { @@ -54,7 +76,7 @@ public int getMonitorIntervalSeconds() { return monitorIntervalSeconds; } - public List getResources() { + public TreeSet getResources() { return resources; } @@ -66,13 +88,13 @@ public void setMonitorIntervalSeconds(int monitorIntervalSeconds) { this.monitorIntervalSeconds = monitorIntervalSeconds; } - public void setResources(List resources) { + public void setResources(TreeSet resources) { this.resources = resources; } @Override public int hashCode() { - return Objects.hash(contextName, monitorIntervalSeconds, resources); + return hash(contextName, monitorIntervalSeconds, resources); } @Override diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java index c906818..a2b675f 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java @@ -22,7 +22,7 @@ import java.net.URL; import java.util.Objects; -public class Resource { +public class Resource implements Comparable { private String location; private String checksum; @@ -70,4 +70,13 @@ public boolean equals(Object obj) { Resource other = (Resource) obj; return Objects.equals(checksum, other.checksum) && Objects.equals(location, other.location); } + + @Override + public int compareTo(Resource other) { + int result = this.location.compareTo(other.location); + if (result == 0) { + return this.checksum.compareTo(other.checksum); + } + return result; + } } 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 015cc88..50d7427 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 @@ -18,39 +18,44 @@ */ package org.apache.accumulo.classloader.lcc; +import static org.apache.accumulo.classloader.lcc.TestUtils.createContextDefinitionFile; +import static org.apache.accumulo.classloader.lcc.TestUtils.testClassFailsToLoad; +import static org.apache.accumulo.classloader.lcc.TestUtils.testClassLoads; +import static org.apache.accumulo.classloader.lcc.TestUtils.updateContextDefinitionFile; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; -import java.io.IOException; -import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.List; +import java.util.TreeSet; +import org.apache.accumulo.classloader.lcc.TestUtils.TestClassInfo; import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.classloader.lcc.definition.Resource; -import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hdfs.MiniDFSCluster; import org.eclipse.jetty.server.Server; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; public class LocalCachingContextClassLoaderFactoryTest { + private static final LocalCachingContextClassLoaderFactory FACTORY = + new LocalCachingContextClassLoaderFactory(); private static final int MONITOR_INTERVAL_SECS = 5; private static MiniDFSCluster hdfs; private static FileSystem fs; @@ -62,6 +67,10 @@ public class LocalCachingContextClassLoaderFactoryTest { private static URL localAllContext; private static URL hdfsAllContext; private static URL jettyAllContext; + private static TestClassInfo classA; + private static TestClassInfo classB; + private static TestClassInfo classC; + private static TestClassInfo classD; @TempDir private static java.nio.file.Path tempDir; @@ -96,18 +105,18 @@ public static void beforeAll() throws Exception { assertTrue(fs.exists(dst)); final URL jarBHdfsLocation = new URL(fs.getUri().toString() + dst.toUri().toString()); - // Put C into Jetty + // Have Jetty serve up files from Jar C directory java.nio.file.Path jarCParentDirectory = Paths.get(jarCOrigLocation.toURI()).getParent(); assertNotNull(jarCParentDirectory); jetty = TestUtils.getJetty(jarCParentDirectory); final URL jarCJettyLocation = jetty.getURI().resolve("TestC.jar").toURL(); // ContextDefinition with all jars - ContextDefinition allJarsDef = createContextDef("all", jarAOrigLocation, jarBHdfsLocation, - jarCJettyLocation, jarDOrigLocation); + ContextDefinition allJarsDef = ContextDefinition.create("all", MONITOR_INTERVAL_SECS, + jarAOrigLocation, jarBHdfsLocation, jarCJettyLocation, jarDOrigLocation); String allJarsDefJson = allJarsDef.toJson(); - System.out.println(allJarsDefJson); + // Create local context definition in jar C directory File localDefFile = new File(jarCParentDirectory.toFile(), "allContextDefinition.json"); Files.writeString(localDefFile.toPath(), allJarsDefJson, StandardOpenOption.CREATE); assertTrue(Files.exists(localDefFile.toPath())); @@ -120,10 +129,15 @@ public static void beforeAll() throws Exception { hdfsAllContext = new URL(fs.getUri().toString() + hdfsDefFile.toUri().toString()); jettyAllContext = jetty.getURI().resolve("allContextDefinition.json").toURL(); + classA = new TestClassInfo("test.TestObjectA", "Hello from A"); + classB = new TestClassInfo("test.TestObjectB", "Hello from B"); + classC = new TestClassInfo("test.TestObjectC", "Hello from C"); + classD = new TestClassInfo("test.TestObjectD", "Hello from D"); } @AfterAll public static void afterAll() throws Exception { + System.clearProperty(Constants.CACHE_DIR_PROPERTY); if (jetty != null) { jetty.stop(); jetty.join(); @@ -133,11 +147,42 @@ public static void afterAll() throws Exception { } } + @AfterEach + public void afterEach() { + FACTORY.resetForTests(); + } + + @Test + public void testCreateFromLocal() throws Exception { + final ClassLoader cl = FACTORY.getClassLoader(localAllContext.toString()); + testClassLoads(cl, classA); + testClassLoads(cl, classB); + testClassLoads(cl, classC); + testClassLoads(cl, classD); + } + + @Test + public void testCreateFromHdfs() throws Exception { + final ClassLoader cl = FACTORY.getClassLoader(hdfsAllContext.toString()); + testClassLoads(cl, classA); + testClassLoads(cl, classB); + testClassLoads(cl, classC); + testClassLoads(cl, classD); + } + + @Test + public void testCreateFromHttp() throws Exception { + final ClassLoader cl = FACTORY.getClassLoader(jettyAllContext.toString()); + testClassLoads(cl, classA); + testClassLoads(cl, classB); + testClassLoads(cl, classC); + testClassLoads(cl, classD); + } + @Test public void testInvalidContextDefinitionURL() { - LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); ContextClassLoaderException ex = - assertThrows(ContextClassLoaderException.class, () -> factory.getClassLoader("/not/a/URL")); + assertThrows(ContextClassLoaderException.class, () -> FACTORY.getClassLoader("/not/a/URL")); assertEquals("Error getting classloader for context: Expected valid URL to context definition " + "file but received: /not/a/URL", ex.getMessage()); } @@ -145,15 +190,11 @@ public void testInvalidContextDefinitionURL() { @Test public void testInitialContextDefinitionEmpty() throws Exception { // Create a new context definition file in HDFS, but with no content - assertTrue(fs.mkdirs(new Path("/contextDefs"))); - final Path empty = new Path("/contextDefs/EmptyContextDefinitionFile.json"); - assertTrue(fs.createNewFile(empty)); - assertTrue(fs.exists(empty)); - final URL emptyDefUrl = new URL(fs.getUri().toString() + empty.toUri().toString()); + final Path def = createContextDefinitionFile(fs, "EmptyContextDefinitionFile.json", null); + final URL emptyDefUrl = new URL(fs.getUri().toString() + def.toUri().toString()); - LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, - () -> factory.getClassLoader(emptyDefUrl.toString())); + () -> FACTORY.getClassLoader(emptyDefUrl.toString())); assertEquals( "Error getting classloader for context: ContextDefinition null for context definition " + "file: " + emptyDefUrl.toString(), @@ -161,231 +202,349 @@ public void testInitialContextDefinitionEmpty() throws Exception { } @Test - public void testInitialContextDefinitionInvalid() throws Exception { + public void testInitialInvalidJson() throws Exception { // Create a new context definition file in HDFS, but with invalid content - assertTrue(fs.mkdirs(new Path("/contextDefs"))); - final Path invalid = new Path("/contextDefs/InvalidContextDefinitionFile.json"); - try (FSDataOutputStream out = fs.create(invalid)) { - ContextDefinition def = createContextDef("invalid", jarAOrigLocation); - out.writeBytes(def.toJson().substring(0, 4)); - } - assertTrue(fs.exists(invalid)); + ContextDefinition def = + ContextDefinition.create("invalid", MONITOR_INTERVAL_SECS, jarAOrigLocation); + // write out invalid json + final Path invalid = createContextDefinitionFile(fs, "InvalidContextDefinitionFile.json", + def.toJson().substring(0, 4)); final URL invalidDefUrl = new URL(fs.getUri().toString() + invalid.toUri().toString()); - LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, - () -> factory.getClassLoader(invalidDefUrl.toString())); + () -> FACTORY.getClassLoader(invalidDefUrl.toString())); assertTrue(ex.getMessage().startsWith( "Error getting classloader for context: com.google.gson.stream.MalformedJsonException")); } @Test public void testInitial() throws Exception { - // Create a new context definition file in HDFS, but with invalid content - assertTrue(fs.mkdirs(new Path("/contextDefs"))); - final Path initial = new Path("/contextDefs/InitialContextDefinitionFile.json"); - try (FSDataOutputStream out = fs.create(initial)) { - ContextDefinition def = createContextDef("initial", jarAOrigLocation); - out.writeBytes(def.toJson()); - } - assertTrue(fs.exists(initial)); + ContextDefinition def = + ContextDefinition.create("initial", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path initial = + createContextDefinitionFile(fs, "InitialContextDefinitionFile.json", def.toJson()); final URL initialDefUrl = new URL(fs.getUri().toString() + initial.toUri().toString()); - LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); - ClassLoader cl = factory.getClassLoader(initialDefUrl.toString()); - @SuppressWarnings("unchecked") - Class clazzA = - (Class) cl.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); + ClassLoader cl = FACTORY.getClassLoader(initialDefUrl.toString()); + + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); + } + + @Test + public void testInitialNonExistentResource() throws Exception { + // copy jarA to some other name + java.nio.file.Path jarAPath = Paths.get(jarAOrigLocation.toURI()); + java.nio.file.Path jarACopy = jarAPath.getParent().resolve("jarACopy.jar"); + assertTrue(!Files.exists(jarACopy)); + Files.copy(jarAPath, jarACopy, StandardCopyOption.REPLACE_EXISTING); + assertTrue(Files.exists(jarACopy)); + + ContextDefinition def = + ContextDefinition.create("initial", MONITOR_INTERVAL_SECS, jarACopy.toUri().toURL()); + + Files.delete(jarACopy); + assertTrue(!Files.exists(jarACopy)); + + final Path initial = createContextDefinitionFile(fs, + "InitialContextDefinitionFileMissingResource.json", def.toJson()); + final URL initialDefUrl = new URL(fs.getUri().toString() + initial.toUri().toString()); + + ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, + () -> FACTORY.getClassLoader(initialDefUrl.toString())); + assertTrue(ex.getMessage().endsWith("jarACopy.jar does not exist.")); + } + + @Test + public void testInitialBadResourceURL() throws Exception { + Resource r = new Resource(); + // remove the file:// prefix from the URL + r.setLocation(jarAOrigLocation.toString().substring(6)); + r.setChecksum("1234"); + TreeSet resources = new TreeSet<>(); + resources.add(r); + + ContextDefinition def = new ContextDefinition(); + def.setContextName("initial"); + def.setMonitorIntervalSeconds(MONITOR_INTERVAL_SECS); + def.setResources(resources); + + final Path initial = createContextDefinitionFile(fs, + "InitialContextDefinitionBadResourceURL.json", def.toJson()); + final URL initialDefUrl = new URL(fs.getUri().toString() + initial.toUri().toString()); + + ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, + () -> FACTORY.getClassLoader(initialDefUrl.toString())); + assertTrue(ex.getMessage().startsWith("Error getting classloader for context: no protocol")); + Throwable t = ex.getCause(); + assertTrue(t instanceof MalformedURLException); + assertTrue(t.getMessage().startsWith("no protocol")); + } + + @Test + public void testInitialBadResourceChecksum() throws Exception { + Resource r = new Resource(); + r.setLocation(jarAOrigLocation.toString()); + r.setChecksum("1234"); + TreeSet resources = new TreeSet<>(); + resources.add(r); + + ContextDefinition def = new ContextDefinition(); + def.setContextName("initial"); + def.setMonitorIntervalSeconds(MONITOR_INTERVAL_SECS); + def.setResources(resources); + + final Path initial = createContextDefinitionFile(fs, + "InitialContextDefinitionBadResourceChecksum.json", def.toJson()); + final URL initialDefUrl = new URL(fs.getUri().toString() + initial.toUri().toString()); + + ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, + () -> FACTORY.getClassLoader(initialDefUrl.toString())); + assertTrue(ex.getMessage().startsWith("Error getting classloader for context: Checksum")); + Throwable t = ex.getCause(); + assertTrue(t instanceof IllegalStateException); + assertTrue( + t.getMessage().endsWith("TestA.jar does not match checksum in context definition 1234")); } @Test public void testUpdate() throws Exception { - // Create a new context definition file in HDFS - assertTrue(fs.mkdirs(new Path("/contextDefs"))); - final Path defFilePath = new Path("/contextDefs/UpdateContextDefinitionFile.json"); - final ContextDefinition def = createContextDef("update", jarAOrigLocation); - try (FSDataOutputStream out = fs.create(defFilePath)) { - out.writeBytes(def.toJson()); - } - assertTrue(fs.exists(defFilePath)); + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateContextDefinitionFile.json", def.toJson()); final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); - final LocalCachingContextClassLoaderFactory factory = - new LocalCachingContextClassLoaderFactory(); - final ClassLoader cl = factory.getClassLoader(updateDefUrl.toString()); - @SuppressWarnings("unchecked") - Class clazzA = - (Class) cl.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); + final ClassLoader cl = FACTORY.getClassLoader(updateDefUrl.toString()); - // Update the contents of the context definition json file - fs.delete(defFilePath, false); - assertFalse(fs.exists(defFilePath)); + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); - ContextDefinition updateDef = createContextDef("update", jarDOrigLocation); - try (FSDataOutputStream out = fs.create(defFilePath)) { - out.writeBytes(updateDef.toJson()); - } - assertTrue(fs.exists(defFilePath)); + // Update the contents of the context definition json file + ContextDefinition updateDef = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarDOrigLocation); + updateContextDefinitionFile(fs, defFilePath, updateDef.toJson()); // wait 2x the monitor interval Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); - final ClassLoader cl2 = factory.getClassLoader(updateDefUrl.toString()); - assertThrows(ClassNotFoundException.class, () -> cl2.loadClass("test.TestObjectA")); + final ClassLoader cl2 = FACTORY.getClassLoader(updateDefUrl.toString()); - @SuppressWarnings("unchecked") - Class clazzD = - (Class) cl2.loadClass("test.TestObjectD"); - test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); - assertEquals("Hello from D", d1.hello()); + assertNotEquals(cl, cl2); + testClassFailsToLoad(cl2, classA); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassLoads(cl2, classD); } @Test - public void testUpdateInvalid() throws Exception { - // Create a new context definition file in HDFS, but with invalid content - assertTrue(fs.mkdirs(new Path("/contextDefs"))); - final Path defFilePath = new Path("/contextDefs/UpdateContextDefinitionFile.json"); - final ContextDefinition def = createContextDef("update", jarAOrigLocation); - try (FSDataOutputStream out = fs.create(defFilePath)) { - out.writeBytes(def.toJson()); - } - assertTrue(fs.exists(defFilePath)); + public void testUpdateContextDefinitionEmpty() throws Exception { + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateEmptyContextDefinitionFile.json", def.toJson()); final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); - final LocalCachingContextClassLoaderFactory factory = - new LocalCachingContextClassLoaderFactory(); - final ClassLoader cl = factory.getClassLoader(updateDefUrl.toString()); - @SuppressWarnings("unchecked") - Class clazzA = - (Class) cl.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); + final ClassLoader cl = FACTORY.getClassLoader(updateDefUrl.toString()); - // Update the contents of the context definition json file - fs.delete(defFilePath, false); - assertFalse(fs.exists(defFilePath)); + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); - ContextDefinition updateDef = createContextDef("update", jarDOrigLocation); - try (FSDataOutputStream out = fs.create(defFilePath)) { - out.writeBytes(updateDef.toJson().substring(0, 4)); - } - assertTrue(fs.exists(defFilePath)); + // Update the contents of the context definition json file with an empty file + updateContextDefinitionFile(fs, defFilePath, null); // wait 2x the monitor interval Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); - final ClassLoader cl2 = factory.getClassLoader(updateDefUrl.toString()); + final ClassLoader cl2 = FACTORY.getClassLoader(updateDefUrl.toString()); - @SuppressWarnings("unchecked") - Class clazzA2 = - (Class) cl2.loadClass("test.TestObjectA"); - test.Test a2 = clazzA2.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a2.hello()); - assertEquals(clazzA, clazzA2); + // validate that the classloader has not updated + assertEquals(cl, cl2); + testClassLoads(cl2, classA); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassFailsToLoad(cl2, classD); - assertThrows(ClassNotFoundException.class, () -> cl2.loadClass("test.TestObjectD")); } - @SuppressWarnings("unchecked") @Test - public void testCreateFromLocal() throws Exception { - LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); - final ClassLoader cl = factory.getClassLoader(localAllContext.toString()); + public void testUpdateNonExistentResource() throws Exception { + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateNonExistentResource.json", def.toJson()); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); - final Class clazzA = - (Class) cl.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); + final ClassLoader cl = FACTORY.getClassLoader(updateDefUrl.toString()); - final Class clazzB = - (Class) cl.loadClass("test.TestObjectB"); - test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); - assertEquals("Hello from B", b1.hello()); + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); - final Class clazzC = - (Class) cl.loadClass("test.TestObjectC"); - test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); - assertEquals("Hello from C", c1.hello()); + // copy jarA to jarACopy + // create a ContextDefinition that references it + // delete jarACopy + java.nio.file.Path jarAPath = Paths.get(jarAOrigLocation.toURI()); + java.nio.file.Path jarACopy = jarAPath.getParent().resolve("jarACopy.jar"); + assertTrue(!Files.exists(jarACopy)); + Files.copy(jarAPath, jarACopy, StandardCopyOption.REPLACE_EXISTING); + assertTrue(Files.exists(jarACopy)); + ContextDefinition def2 = + ContextDefinition.create("initial", MONITOR_INTERVAL_SECS, jarACopy.toUri().toURL()); + Files.delete(jarACopy); + assertTrue(!Files.exists(jarACopy)); - final Class clazzD = - (Class) cl.loadClass("test.TestObjectD"); - test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); - assertEquals("Hello from D", d1.hello()); + updateContextDefinitionFile(fs, defFilePath, def2.toJson()); + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl2 = FACTORY.getClassLoader(updateDefUrl.toString()); + + // validate that the classloader has not updated + assertEquals(cl, cl2); + testClassLoads(cl2, classA); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassFailsToLoad(cl2, classD); } - @SuppressWarnings("unchecked") @Test - public void testCreateFromHdfs() throws Exception { - LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); - final ClassLoader cl = factory.getClassLoader(hdfsAllContext.toString()); + public void testUpdateBadResourceChecksum() throws Exception { + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateBadResourceChecksum.json", def.toJson()); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); + + final ClassLoader cl = FACTORY.getClassLoader(updateDefUrl.toString()); + + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); - final Class clazzA = - (Class) cl.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); + Resource r = new Resource(); + r.setLocation(jarAOrigLocation.toString()); + r.setChecksum("1234"); + TreeSet resources = new TreeSet<>(); + resources.add(r); - final Class clazzB = - (Class) cl.loadClass("test.TestObjectB"); - test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); - assertEquals("Hello from B", b1.hello()); + ContextDefinition def2 = new ContextDefinition(); + def2.setContextName("update"); + def2.setMonitorIntervalSeconds(MONITOR_INTERVAL_SECS); + def2.setResources(resources); - final Class clazzC = - (Class) cl.loadClass("test.TestObjectC"); - test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); - assertEquals("Hello from C", c1.hello()); + updateContextDefinitionFile(fs, defFilePath, def2.toJson()); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); - final Class clazzD = - (Class) cl.loadClass("test.TestObjectD"); - test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); - assertEquals("Hello from D", d1.hello()); + final ClassLoader cl2 = FACTORY.getClassLoader(updateDefUrl.toString()); + // validate that the classloader has not updated + assertEquals(cl, cl2); + testClassLoads(cl2, classA); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassFailsToLoad(cl2, classD); } - @SuppressWarnings("unchecked") @Test - public void testCreateFromHttp() throws Exception { - LocalCachingContextClassLoaderFactory factory = new LocalCachingContextClassLoaderFactory(); - final ClassLoader cl = factory.getClassLoader(jettyAllContext.toString()); + public void testUpdateBadResourceURL() throws Exception { + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateBadResourceChecksum.json", def.toJson()); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); + + final ClassLoader cl = FACTORY.getClassLoader(updateDefUrl.toString()); + + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); - final Class clazzA = - (Class) cl.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); + Resource r = new Resource(); + // remove the file:// prefix from the URL + r.setLocation(jarAOrigLocation.toString().substring(6)); + r.setChecksum("1234"); + TreeSet resources = new TreeSet<>(); + resources.add(r); - final Class clazzB = - (Class) cl.loadClass("test.TestObjectB"); - test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); - assertEquals("Hello from B", b1.hello()); + ContextDefinition def2 = new ContextDefinition(); + def2.setContextName("initial"); + def2.setMonitorIntervalSeconds(MONITOR_INTERVAL_SECS); + def2.setResources(resources); - final Class clazzC = - (Class) cl.loadClass("test.TestObjectC"); - test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); - assertEquals("Hello from C", c1.hello()); + updateContextDefinitionFile(fs, defFilePath, def2.toJson()); - final Class clazzD = - (Class) cl.loadClass("test.TestObjectD"); - test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); - assertEquals("Hello from D", d1.hello()); + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl2 = FACTORY.getClassLoader(updateDefUrl.toString()); + // validate that the classloader has not updated + assertEquals(cl, cl2); + testClassLoads(cl2, classA); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassFailsToLoad(cl2, classD); } - private static ContextDefinition createContextDef(String contextName, URL... sources) - throws ContextClassLoaderException, IOException { - List resources = new ArrayList<>(); - for (URL u : sources) { - FileResolver resolver = FileResolver.resolve(u); - try (InputStream is = resolver.getInputStream()) { - String checksum = Constants.getChecksummer().digestAsHex(is); - resources.add(new Resource(u.toString(), checksum)); - } - } - return new ContextDefinition(contextName, MONITOR_INTERVAL_SECS, resources); + @Test + public void testUpdateInvalidJson() throws Exception { + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateInvalidContextDefinitionFile.json", def.toJson()); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); + + final ClassLoader cl = FACTORY.getClassLoader(updateDefUrl.toString()); + + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); + + ContextDefinition updateDef = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarDOrigLocation); + updateContextDefinitionFile(fs, defFilePath, updateDef.toJson().substring(0, 4)); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl2 = FACTORY.getClassLoader(updateDefUrl.toString()); + + // validate that the classloader has not updated + assertEquals(cl, cl2); + testClassLoads(cl2, classA); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassFailsToLoad(cl2, classD); + + // Re-write the updated context definition such that it is now valid + updateContextDefinitionFile(fs, defFilePath, updateDef.toJson()); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl3 = FACTORY.getClassLoader(updateDefUrl.toString()); + + assertEquals(cl, cl2); + assertNotEquals(cl, cl3); + testClassFailsToLoad(cl3, classA); + testClassFailsToLoad(cl3, classB); + testClassFailsToLoad(cl3, classC); + testClassLoads(cl3, classD); } } diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java index 877cfd4..e4b3032 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java @@ -18,19 +18,20 @@ */ package org.apache.accumulo.classloader.lcc; +import static org.apache.accumulo.classloader.lcc.TestUtils.testClassFailsToLoad; +import static org.apache.accumulo.classloader.lcc.TestUtils.testClassLoads; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; +import java.util.TreeSet; +import org.apache.accumulo.classloader.lcc.TestUtils.TestClassInfo; import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.classloader.lcc.definition.Resource; import org.apache.hadoop.fs.FileSystem; @@ -50,6 +51,10 @@ public class LocalCachingContextTest { private static MiniDFSCluster hdfs; private static Server jetty; private static ContextDefinition def; + private static TestClassInfo classA; + private static TestClassInfo classB; + private static TestClassInfo classC; + private static TestClassInfo classD; @TempDir private static java.nio.file.Path tempDir; @@ -86,7 +91,7 @@ public static void beforeAll() throws Exception { final URL jarCNewLocation = jetty.getURI().resolve("TestC.jar").toURL(); // Create ContextDefinition with all three resources - final List resources = new ArrayList<>(); + final TreeSet resources = new TreeSet<>(); resources.add(new Resource(jarAOrigLocation.toString(), TestUtils.computeResourceChecksum(jarAOrigLocation))); resources.add(new Resource(jarBNewLocation.toString(), @@ -95,13 +100,22 @@ public static void beforeAll() throws Exception { TestUtils.computeResourceChecksum(jarCOrigLocation))); def = new ContextDefinition(CONTEXT_NAME, MONITOR_INTERVAL_SECS, resources); + classA = new TestClassInfo("test.TestObjectA", "Hello from A"); + classB = new TestClassInfo("test.TestObjectB", "Hello from B"); + classC = new TestClassInfo("test.TestObjectC", "Hello from C"); + classD = new TestClassInfo("test.TestObjectD", "Hello from D"); } @AfterAll public static void afterAll() throws Exception { - jetty.stop(); - jetty.join(); - hdfs.shutdown(); + System.clearProperty(Constants.CACHE_DIR_PROPERTY); + if (jetty != null) { + jetty.stop(); + jetty.join(); + } + if (hdfs != null) { + hdfs.shutdown(); + } } @Test @@ -120,7 +134,6 @@ public void testInitialize() throws Exception { } } - @SuppressWarnings("unchecked") @Test public void testClassLoader() throws Exception { @@ -128,23 +141,11 @@ public void testClassLoader() throws Exception { lcccl.initialize(); ClassLoader contextClassLoader = lcccl.getClassloader(); - Class clazzA = - (Class) contextClassLoader.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); - - Class clazzB = - (Class) contextClassLoader.loadClass("test.TestObjectB"); - test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); - assertEquals("Hello from B", b1.hello()); - - Class clazzC = - (Class) contextClassLoader.loadClass("test.TestObjectC"); - test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); - assertEquals("Hello from C", c1.hello()); + testClassLoads(contextClassLoader, classA); + testClassLoads(contextClassLoader, classB); + testClassLoads(contextClassLoader, classC); } - @SuppressWarnings("unchecked") @Test public void testUpdate() throws Exception { @@ -153,24 +154,13 @@ public void testUpdate() throws Exception { final ClassLoader contextClassLoader = lcccl.getClassloader(); - final Class clazzA = - (Class) contextClassLoader.loadClass("test.TestObjectA"); - test.Test a1 = clazzA.getDeclaredConstructor().newInstance(); - assertEquals("Hello from A", a1.hello()); - - final Class clazzB = - (Class) contextClassLoader.loadClass("test.TestObjectB"); - test.Test b1 = clazzB.getDeclaredConstructor().newInstance(); - assertEquals("Hello from B", b1.hello()); + testClassLoads(contextClassLoader, classA); + testClassLoads(contextClassLoader, classB); + testClassLoads(contextClassLoader, classC); - final Class clazzC = - (Class) contextClassLoader.loadClass("test.TestObjectC"); - test.Test c1 = clazzC.getDeclaredConstructor().newInstance(); - assertEquals("Hello from C", c1.hello()); - - List updatedResources = new ArrayList<>(def.getResources()); + TreeSet updatedResources = new TreeSet<>(def.getResources()); assertEquals(3, updatedResources.size()); - updatedResources.remove(2); // remove C + updatedResources.remove(updatedResources.last()); // remove C // Add D final URL jarDOrigLocation = @@ -196,26 +186,11 @@ public void testUpdate() throws Exception { final ClassLoader updatedContextClassLoader = lcccl.getClassloader(); - final Class clazzA2 = - (Class) updatedContextClassLoader.loadClass("test.TestObjectA"); - test.Test a2 = clazzA2.getDeclaredConstructor().newInstance(); - assertNotEquals(clazzA, clazzA2); - assertEquals("Hello from A", a2.hello()); - - final Class clazzB2 = - (Class) updatedContextClassLoader.loadClass("test.TestObjectB"); - test.Test b2 = clazzB2.getDeclaredConstructor().newInstance(); - assertNotEquals(clazzB, clazzB2); - assertEquals("Hello from B", b2.hello()); - - assertThrows(ClassNotFoundException.class, - () -> updatedContextClassLoader.loadClass("test.TestObjectC")); - - final Class clazzD = - (Class) updatedContextClassLoader.loadClass("test.TestObjectD"); - test.Test d1 = clazzD.getDeclaredConstructor().newInstance(); - assertEquals("Hello from D", d1.hello()); - + assertNotEquals(contextClassLoader, updatedContextClassLoader); + testClassLoads(updatedContextClassLoader, classA); + testClassLoads(updatedContextClassLoader, classB); + testClassFailsToLoad(updatedContextClassLoader, classC); + testClassLoads(updatedContextClassLoader, classD); } } diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java index f8b38ab..901ed1a 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java @@ -18,15 +18,22 @@ */ package org.apache.accumulo.classloader.lcc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; import org.apache.hadoop.hdfs.DFSConfigKeys; import org.apache.hadoop.hdfs.MiniDFSCluster; import org.eclipse.jetty.server.Server; @@ -37,6 +44,72 @@ public class TestUtils { + public static class TestClassInfo { + private final String className; + private final String helloOutput; + + public TestClassInfo(String className, String helloOutput) { + super(); + this.className = className; + this.helloOutput = helloOutput; + } + + public String getClassName() { + return className; + } + + public String getHelloOutput() { + return helloOutput; + } + } + + public static Path createContextDefinitionFile(FileSystem fs, String name, String contents) + throws Exception { + Path baseHdfsPath = new Path("/contextDefs"); + assertTrue(fs.mkdirs(baseHdfsPath)); + Path newContextDefinitionFile = new Path(baseHdfsPath, name); + + if (contents == null) { + assertTrue(fs.createNewFile(newContextDefinitionFile)); + } else { + try (FSDataOutputStream out = fs.create(newContextDefinitionFile)) { + out.writeBytes(contents); + } + } + assertTrue(fs.exists(newContextDefinitionFile)); + return newContextDefinitionFile; + } + + public static void updateContextDefinitionFile(FileSystem fs, Path defFilePath, String contents) + throws Exception { + // Update the contents of the context definition json file + assertTrue(fs.exists(defFilePath)); + fs.delete(defFilePath, false); + assertFalse(fs.exists(defFilePath)); + + if (contents == null) { + assertTrue(fs.createNewFile(defFilePath)); + } else { + try (FSDataOutputStream out = fs.create(defFilePath)) { + out.writeBytes(contents); + } + } + assertTrue(fs.exists(defFilePath)); + + } + + public static void testClassLoads(ClassLoader cl, TestClassInfo tci) throws Exception { + @SuppressWarnings("unchecked") + Class clazz = + (Class) cl.loadClass(tci.getClassName()); + test.Test impl = clazz.getDeclaredConstructor().newInstance(); + assertEquals(tci.getHelloOutput(), impl.hello()); + } + + public static void testClassFailsToLoad(ClassLoader cl, TestClassInfo tci) throws Exception { + assertThrows(ClassNotFoundException.class, () -> cl.loadClass(tci.getClassName())); + } + private static String computeDatanodeDirectoryPermission() { // MiniDFSCluster will check the permissions on the data directories, but does not // do a good job of setting them properly. We need to get the users umask and set @@ -82,7 +155,7 @@ public static MiniDFSCluster getMiniCluster() throws IOException { return cluster; } - public static Server getJetty(Path resourceDirectory) throws Exception { + public static Server getJetty(java.nio.file.Path resourceDirectory) throws Exception { PathResource directory = new PathResource(resourceDirectory); ResourceHandler handler = new ResourceHandler(); handler.setBaseResource(directory); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java index 997638a..521ba0a 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java @@ -33,6 +33,7 @@ import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.classloader.lcc.cache.CacheUtils.LockInfo; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -53,6 +54,11 @@ public void beforeEach() { System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); } + @AfterEach + public void afterEach() { + System.clearProperty(Constants.CACHE_DIR_PROPERTY); + } + @Test public void testPropertyNotSet() { System.clearProperty(Constants.CACHE_DIR_PROPERTY); diff --git a/modules/local-caching-classloader/src/test/resources/log4j2-test.properties b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties index 9a58884..3d5abd6 100644 --- a/modules/local-caching-classloader/src/test/resources/log4j2-test.properties +++ b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties @@ -17,7 +17,7 @@ # under the License. # -status = info +status = error dest = err name = LocalCachineClassLoaderTestLoggingProperties From 981fed80be90a43e3631714e4ce854b488dad4d2 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 20 Nov 2025 20:27:22 +0000 Subject: [PATCH 18/31] Changes from merging in main, fixing conflicts and new build issues --- .../classloader/lcc/LocalCachingContext.java | 9 ++++++--- .../LocalCachingContextClassLoaderFactory.java | 10 +++++++--- .../classloader/lcc/cache/CacheUtils.java | 6 +----- .../lcc/definition/ContextDefinition.java | 13 ++++++++----- .../classloader/lcc/definition/Resource.java | 9 ++++++--- .../lcc/resolvers/FileResolver.java | 9 ++++++--- .../lcc/resolvers/LocalFileResolver.java | 5 ++--- ...alCachingContextClassLoaderFactoryTest.java | 18 +++++++++++------- .../lcc/LocalCachingContextTest.java | 8 ++++---- .../accumulo/classloader/lcc/TestUtils.java | 4 ++-- .../classloader/lcc/cache/CacheUtilsTest.java | 9 ++++----- .../lcc/resolvers/FileResolversTest.java | 7 +++---- 12 files changed, 60 insertions(+), 47 deletions(-) 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 0082bbc..8392efa 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 @@ -79,12 +79,15 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } ClassPathElement other = (ClassPathElement) obj; return Objects.equals(localCachedCopyDigest, other.localCachedCopyDigest) && Objects.equals(localCachedCopyLocation, other.localCachedCopyLocation) 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 bcb4e42..90f6fed 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 @@ -179,10 +179,14 @@ public ClassLoader getClassLoader(final String contextLocation) if (t != null && t instanceof InterruptedException) { Thread.currentThread().interrupt(); } - if (t != null && t instanceof ContextClassLoaderException) { - throw (ContextClassLoaderException) t; + if (t != null) { + if (t instanceof ContextClassLoaderException) { + throw (ContextClassLoaderException) t; + } else { + throw new ContextClassLoaderException(t.getMessage(), t); + } } else { - throw new ContextClassLoaderException(t.getMessage(), t); + throw new ContextClassLoaderException(re.getMessage(), re); } } } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index b1a3004..6f7b65a 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -33,7 +33,6 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; @@ -44,8 +43,6 @@ import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - public class CacheUtils { private static final Set CACHE_DIR_PERMS = @@ -64,7 +61,6 @@ public LockInfo(FileChannel channel, FileLock lock) { this.lock = requireNonNull(lock, "lock must be supplied"); } - @SuppressFBWarnings(value = "EI_EXPOSE_REP") FileChannel getChannel() { return channel; } @@ -94,7 +90,7 @@ public static Path createBaseCacheDir() throws IOException, ContextClassLoaderEx if (cacheDir == null) { throw new ContextClassLoaderException("System property " + prop + " not set."); } - return mkdir(Paths.get(URI.create(cacheDir))); + return mkdir(Path.of(URI.create(cacheDir))); } public static Path createOrGetContextCacheDir(final String contextName) 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 22d87e5..d1d180c 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 @@ -36,7 +36,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -@SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) +@SuppressFBWarnings(value = {"EI_EXPOSE_REP"}) public class ContextDefinition { public static ContextDefinition create(String contextName, int monitorIntervalSecs, @@ -53,7 +53,7 @@ public static ContextDefinition create(String contextName, int monitorIntervalSe } private String contextName; - private int monitorIntervalSeconds; + private volatile int monitorIntervalSeconds; private TreeSet resources; private volatile transient byte[] checksum = null; @@ -99,12 +99,15 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } ContextDefinition other = (ContextDefinition) obj; return Objects.equals(contextName, other.contextName) && monitorIntervalSeconds == other.monitorIntervalSeconds diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java index a2b675f..119824e 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java @@ -61,12 +61,15 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } Resource other = (Resource) obj; return Objects.equals(checksum, other.checksum) && Objects.equals(location, other.location); } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java index 3fcc261..88014df 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java @@ -66,12 +66,15 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } FileResolver other = (FileResolver) obj; return Objects.equals(url, other.url); } diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java index 5aa10de..dfa45d6 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java @@ -26,7 +26,6 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; import org.apache.hadoop.shaded.org.apache.commons.io.FileUtils; @@ -43,8 +42,8 @@ public LocalFileResolver(URL url) throws ContextClassLoaderException { } try { final URI uri = url.toURI(); - final Path path = Paths.get(uri); - if (Files.notExists(Paths.get(uri))) { + final Path path = Path.of(uri); + if (Files.notExists(Path.of(uri))) { throw new ContextClassLoaderException("File: " + url + " does not exist."); } file = path.toFile(); 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 50d7427..b64c4a0 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 @@ -32,7 +32,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; -import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.TreeSet; @@ -106,7 +105,8 @@ public static void beforeAll() throws Exception { final URL jarBHdfsLocation = new URL(fs.getUri().toString() + dst.toUri().toString()); // Have Jetty serve up files from Jar C directory - java.nio.file.Path jarCParentDirectory = Paths.get(jarCOrigLocation.toURI()).getParent(); + java.nio.file.Path jarCParentDirectory = + java.nio.file.Path.of(jarCOrigLocation.toURI()).getParent(); assertNotNull(jarCParentDirectory); jetty = TestUtils.getJetty(jarCParentDirectory); final URL jarCJettyLocation = jetty.getURI().resolve("TestC.jar").toURL(); @@ -117,7 +117,7 @@ public static void beforeAll() throws Exception { String allJarsDefJson = allJarsDef.toJson(); // Create local context definition in jar C directory - File localDefFile = new File(jarCParentDirectory.toFile(), "allContextDefinition.json"); + File localDefFile = jarCParentDirectory.resolve("allContextDefinition.json").toFile(); Files.writeString(localDefFile.toPath(), allJarsDefJson, StandardOpenOption.CREATE); assertTrue(Files.exists(localDefFile.toPath())); @@ -236,8 +236,10 @@ public void testInitial() throws Exception { @Test public void testInitialNonExistentResource() throws Exception { // copy jarA to some other name - java.nio.file.Path jarAPath = Paths.get(jarAOrigLocation.toURI()); - java.nio.file.Path jarACopy = jarAPath.getParent().resolve("jarACopy.jar"); + java.nio.file.Path jarAPath = java.nio.file.Path.of(jarAOrigLocation.toURI()); + java.nio.file.Path jarAPathParent = jarAPath.getParent(); + assertNotNull(jarAPathParent); + java.nio.file.Path jarACopy = jarAPathParent.resolve("jarACopy.jar"); assertTrue(!Files.exists(jarACopy)); Files.copy(jarAPath, jarACopy, StandardCopyOption.REPLACE_EXISTING); assertTrue(Files.exists(jarACopy)); @@ -392,8 +394,10 @@ public void testUpdateNonExistentResource() throws Exception { // copy jarA to jarACopy // create a ContextDefinition that references it // delete jarACopy - java.nio.file.Path jarAPath = Paths.get(jarAOrigLocation.toURI()); - java.nio.file.Path jarACopy = jarAPath.getParent().resolve("jarACopy.jar"); + java.nio.file.Path jarAPath = java.nio.file.Path.of(jarAOrigLocation.toURI()); + java.nio.file.Path jarAPathParent = jarAPath.getParent(); + assertNotNull(jarAPathParent); + java.nio.file.Path jarACopy = jarAPathParent.resolve("jarACopy.jar"); assertTrue(!Files.exists(jarACopy)); Files.copy(jarAPath, jarACopy, StandardCopyOption.REPLACE_EXISTING); assertTrue(Files.exists(jarACopy)); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java index e4b3032..e2bb0ec 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java @@ -28,7 +28,6 @@ import java.net.URL; import java.nio.file.Files; -import java.nio.file.Paths; import java.util.TreeSet; import org.apache.accumulo.classloader.lcc.TestUtils.TestClassInfo; @@ -86,7 +85,8 @@ public static void beforeAll() throws Exception { final URL jarBNewLocation = new URL(fs.getUri().toString() + dst.toUri().toString()); // Put C into Jetty - java.nio.file.Path jarCParentDirectory = Paths.get(jarCOrigLocation.toURI()).getParent(); + java.nio.file.Path jarCParentDirectory = + java.nio.file.Path.of(jarCOrigLocation.toURI()).getParent(); jetty = TestUtils.getJetty(jarCParentDirectory); final URL jarCNewLocation = jetty.getURI().resolve("TestC.jar").toURL(); @@ -124,7 +124,7 @@ public void testInitialize() throws Exception { lcccl.initialize(); // Confirm the 3 jars are cached locally - final java.nio.file.Path base = Paths.get(tempDir.resolve("base").toUri()); + final java.nio.file.Path base = java.nio.file.Path.of(tempDir.resolve("base").toUri()); assertTrue(Files.exists(base)); assertTrue(Files.exists(base.resolve(CONTEXT_NAME))); for (Resource r : def.getResources()) { @@ -174,7 +174,7 @@ public void testUpdate() throws Exception { lcccl.update(updatedDef); // Confirm the 3 jars are cached locally - final java.nio.file.Path base = Paths.get(tempDir.resolve("base").toUri()); + final java.nio.file.Path base = java.nio.file.Path.of(tempDir.resolve("base").toUri()); assertTrue(Files.exists(base)); assertTrue(Files.exists(base.resolve(CONTEXT_NAME))); for (Resource r : updatedDef.getResources()) { diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java index 901ed1a..65b7c3e 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java @@ -18,6 +18,7 @@ */ package org.apache.accumulo.classloader.lcc; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -28,7 +29,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; -import java.nio.charset.StandardCharsets; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataOutputStream; @@ -118,7 +118,7 @@ private static String computeDatanodeDirectoryPermission() { try { Process p = Runtime.getRuntime().exec("/bin/sh -c umask"); try (BufferedReader bri = - new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { + new BufferedReader(new InputStreamReader(p.getInputStream(), UTF_8))) { String line = bri.readLine(); p.waitFor(); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java index 521ba0a..2f1eb86 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java @@ -27,7 +27,6 @@ import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.concurrent.atomic.AtomicReference; import org.apache.accumulo.classloader.lcc.Constants; @@ -70,7 +69,7 @@ public void testPropertyNotSet() { @Test public void testCreateBaseDir() throws Exception { - final Path base = Paths.get(tempDir.resolve("base").toUri()); + final Path base = Path.of(tempDir.resolve("base").toUri()); try { assertFalse(Files.exists(base)); CacheUtils.createBaseCacheDir(); @@ -82,7 +81,7 @@ public void testCreateBaseDir() throws Exception { @Test public void testCreateBaseDirMultipleTimes() throws Exception { - final Path base = Paths.get(tempDir.resolve("base").toUri()); + final Path base = Path.of(tempDir.resolve("base").toUri()); try { assertFalse(Files.exists(base)); CacheUtils.createBaseCacheDir(); @@ -97,7 +96,7 @@ public void testCreateBaseDirMultipleTimes() throws Exception { @Test public void createOrGetContextCacheDir() throws Exception { - final Path base = Paths.get(tempDir.resolve("base").toUri()); + final Path base = Path.of(tempDir.resolve("base").toUri()); try { assertFalse(Files.exists(base)); CacheUtils.createOrGetContextCacheDir("context1"); @@ -118,7 +117,7 @@ public void createOrGetContextCacheDir() throws Exception { @Test public void testLock() throws Exception { - final Path base = Paths.get(tempDir.resolve("base").toUri()); + final Path base = Path.of(tempDir.resolve("base").toUri()); final Path cx1 = base.resolve("context1"); try { assertFalse(Files.exists(base)); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java index 5bec589..09d4ba2 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java @@ -26,7 +26,6 @@ import java.io.InputStream; import java.net.URL; import java.nio.file.Files; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import org.apache.accumulo.classloader.lcc.TestUtils; @@ -61,7 +60,7 @@ private long getFileSize(FileResolver resolver) throws IOException, ContextClass public void testLocalFile() throws Exception { URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar"); assertNotNull(jarPath); - java.nio.file.Path p = Paths.get(jarPath.toURI()); + java.nio.file.Path p = java.nio.file.Path.of(jarPath.toURI()); final long origFileSize = getFileSize(p); FileResolver resolver = FileResolver.resolve(jarPath); assertTrue(resolver instanceof LocalFileResolver); @@ -75,7 +74,7 @@ public void testHttpFile() throws Exception { URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar"); assertNotNull(jarPath); - java.nio.file.Path p = Paths.get(jarPath.toURI()); + java.nio.file.Path p = java.nio.file.Path.of(jarPath.toURI()); final long origFileSize = getFileSize(p); Server jetty = TestUtils.getJetty(p.getParent()); @@ -96,7 +95,7 @@ public void testHdfsFile() throws Exception { URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar"); assertNotNull(jarPath); - java.nio.file.Path p = Paths.get(jarPath.toURI()); + java.nio.file.Path p = java.nio.file.Path.of(jarPath.toURI()); final long origFileSize = getFileSize(p); MiniDFSCluster cluster = TestUtils.getMiniCluster(); From 303a1da1b79f370027de5f8664fd3fd20abce32c Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 20 Nov 2025 21:56:27 +0000 Subject: [PATCH 19/31] Add new tests --- ...LocalCachingContextClassLoaderFactory.java | 1 - ...lCachingContextClassLoaderFactoryTest.java | 113 ++++++++++++++++++ .../src/test/shell/makeTestJars.sh | 7 ++ 3 files changed, 120 insertions(+), 1 deletion(-) 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 90f6fed..83866a9 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 @@ -127,7 +127,6 @@ private void monitorContext(final String contextLocation, int interval) { } else { LOG.trace("Context definition for {} has not changed", contextLocation); } - monitorContext(contextLocation, update.getMonitorIntervalSeconds()); } catch (ContextClassLoaderException | InterruptedException | IOException | NoSuchAlgorithmException | URISyntaxException e) { LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", 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 b64c4a0..7f4b40f 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 @@ -34,6 +34,9 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.TreeSet; import org.apache.accumulo.classloader.lcc.TestUtils.TestClassInfo; @@ -63,6 +66,7 @@ public class LocalCachingContextClassLoaderFactoryTest { private static URL jarBOrigLocation; private static URL jarCOrigLocation; private static URL jarDOrigLocation; + private static URL jarEOrigLocation; private static URL localAllContext; private static URL hdfsAllContext; private static URL jettyAllContext; @@ -92,6 +96,9 @@ public static void beforeAll() throws Exception { jarDOrigLocation = LocalCachingContextClassLoaderFactoryTest.class.getResource("/ClassLoaderTestD/TestD.jar"); assertNotNull(jarDOrigLocation); + jarEOrigLocation = + LocalCachingContextClassLoaderFactoryTest.class.getResource("/ClassLoaderTestE/TestE.jar"); + assertNotNull(jarEOrigLocation); // Put B into HDFS hdfs = TestUtils.getMiniCluster(); @@ -344,6 +351,43 @@ public void testUpdate() throws Exception { testClassLoads(cl2, classD); } + @Test + public void testUpdateSameClassNameDifferentContent() throws Exception { + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateContextDefinitionFile.json", def.toJson()); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); + + final ClassLoader cl = FACTORY.getClassLoader(updateDefUrl.toString()); + + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); + + // Update the contents of the context definition json file + ContextDefinition updateDef = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarEOrigLocation); + updateContextDefinitionFile(fs, defFilePath, updateDef.toJson()); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl2 = FACTORY.getClassLoader(updateDefUrl.toString()); + + assertNotEquals(cl, cl2); + + @SuppressWarnings("unchecked") + Class clazz = + (Class) cl2.loadClass("test.TestObjectA"); + test.Test impl = clazz.getDeclaredConstructor().newInstance(); + assertEquals("Hello from E", impl.hello()); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassFailsToLoad(cl2, classD); + } + @Test public void testUpdateContextDefinitionEmpty() throws Exception { final ContextDefinition def = @@ -551,4 +595,73 @@ public void testUpdateInvalidJson() throws Exception { testClassLoads(cl3, classD); } + @Test + public void testChangingContext() throws Exception { + ContextDefinition def = ContextDefinition.create("update", MONITOR_INTERVAL_SECS, + jarAOrigLocation, jarBOrigLocation, jarCOrigLocation, jarDOrigLocation); + final Path update = + createContextDefinitionFile(fs, "UpdateChangingContextDefinition.json", def.toJson()); + final URL updatedDefUrl = new URL(fs.getUri().toString() + update.toUri().toString()); + + final ClassLoader cl = FACTORY.getClassLoader(updatedDefUrl.toString()); + testClassLoads(cl, classA); + testClassLoads(cl, classB); + testClassLoads(cl, classC); + testClassLoads(cl, classD); + + final List masterList = new ArrayList<>(); + masterList.add(jarAOrigLocation); + masterList.add(jarBOrigLocation); + masterList.add(jarCOrigLocation); + masterList.add(jarDOrigLocation); + + List priorList = masterList; + ClassLoader priorCL = cl; + + for (int i = 0; i < 20; i++) { + final List updatedList = new ArrayList<>(masterList); + Collections.shuffle(updatedList); + final URL removed = updatedList.remove(0); + + // Update the contents of the context definition json file + ContextDefinition updateDef = ContextDefinition.create("update", MONITOR_INTERVAL_SECS, + updatedList.toArray(new URL[] {})); + updateContextDefinitionFile(fs, update, updateDef.toJson()); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader updatedClassLoader = FACTORY.getClassLoader(updatedDefUrl.toString()); + + if (updatedList.equals(priorList)) { + assertEquals(priorCL, updatedClassLoader); + } else { + assertNotEquals(cl, updatedClassLoader); + for (URL u : updatedList) { + if (u.equals(jarAOrigLocation)) { + testClassLoads(updatedClassLoader, classA); + } else if (u.equals(jarBOrigLocation)) { + testClassLoads(updatedClassLoader, classB); + } else if (u.equals(jarCOrigLocation)) { + testClassLoads(updatedClassLoader, classC); + } else if (u.equals(jarDOrigLocation)) { + testClassLoads(updatedClassLoader, classD); + } + } + } + if (removed.equals(jarAOrigLocation)) { + testClassFailsToLoad(updatedClassLoader, classA); + } else if (removed.equals(jarBOrigLocation)) { + testClassFailsToLoad(updatedClassLoader, classB); + } else if (removed.equals(jarCOrigLocation)) { + testClassFailsToLoad(updatedClassLoader, classC); + } else if (removed.equals(jarDOrigLocation)) { + testClassFailsToLoad(updatedClassLoader, classD); + } + priorCL = updatedClassLoader; + priorList = updatedList; + } + + } + } diff --git a/modules/local-caching-classloader/src/test/shell/makeTestJars.sh b/modules/local-caching-classloader/src/test/shell/makeTestJars.sh index 815cf28..77a7fd0 100755 --- a/modules/local-caching-classloader/src/test/shell/makeTestJars.sh +++ b/modules/local-caching-classloader/src/test/shell/makeTestJars.sh @@ -31,3 +31,10 @@ for x in A B C D; do rm -r target/generated-sources/$x done +# Create a one-off jar that uses the A class name, but returns the E content for the hello method +# this will be located in the ClassLoaderTestE directory +mkdir -p target/generated-sources/E/test target/test-classes/ClassLoaderTestE +sed "s/XXX/A/" target/generated-sources/E/test/TestObjectA.java +sed -i "s/Hello from A/Hello from E/" target/generated-sources/E/test/TestObjectA.java +"$JAVA_HOME/bin/javac" --release 11 -cp target/test-classes target/generated-sources/E/test/TestObjectA.java -d target/generated-sources/E +"$JAVA_HOME/bin/jar" -cf target/test-classes/ClassLoaderTestE/TestE.jar -C target/generated-sources/E test/TestObjectA.class From 0067f8bc24e10fc9c51cb93f98750954b530f10f Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 21 Nov 2025 06:47:41 -0500 Subject: [PATCH 20/31] Update modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java Co-authored-by: Keith Turner --- .../classloader/lcc/LocalCachingContextClassLoaderFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 83866a9..9631226 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 @@ -56,7 +56,7 @@ *

* As this class processes the ContextDefinition it fetches the contents of the resource from the * resource URL and caches it in a directory on the local filesystem. This class uses the value of - * the system property {@link Constants#CACHE_DIR_PROPERTY} as the root directory and creates a + * the system property {@value Constants#CACHE_DIR_PROPERTY} as the root directory and creates a * sub-directory for each context name. Each context cache directory contains a lock file and a copy * of each fetched resource that is named using the following format: fileName_checksum. *

From 10677ec89d38586dfb9e2d63b4e1bbc789a1f818 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 21 Nov 2025 06:50:09 -0500 Subject: [PATCH 21/31] Update modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java Co-authored-by: Keith Turner --- .../org/apache/accumulo/classloader/lcc/cache/CacheUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index 6f7b65a..3d952f2 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -46,7 +46,7 @@ public class CacheUtils { private static final Set CACHE_DIR_PERMS = - EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, OTHERS_READ); + EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); private static final FileAttribute> PERMISSIONS = PosixFilePermissions.asFileAttribute(CACHE_DIR_PERMS); private static final String lockFileName = "lock_file"; From f2bd69ef6b7e9cccb9913b78af11600da93e201c Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 21 Nov 2025 12:35:30 +0000 Subject: [PATCH 22/31] Applied PR suggestions, updated README --- modules/local-caching-classloader/README.md | 31 +++++++++++++++++-- modules/local-caching-classloader/pom.xml | 5 --- .../accumulo/classloader/lcc/Constants.java | 2 +- .../classloader/lcc/LocalCachingContext.java | 3 +- ...LocalCachingContextClassLoaderFactory.java | 7 ++--- .../classloader/lcc/cache/CacheUtils.java | 2 -- .../lcc/resolvers/LocalFileResolver.java | 8 ++--- ...lCachingContextClassLoaderFactoryTest.java | 23 ++++++++------ 8 files changed, 53 insertions(+), 28 deletions(-) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index fcd36ca..48dc487 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -21,7 +21,7 @@ The LocalCachingContextClassLoaderFactory is an Accumulo ContextClassLoaderFacto LocalCachingContext. The `LocalCachingContextClassLoaderFactory.getClassLoader(String)` method expects the method argument to be a valid `file`, `hdfs`, `http` or `https` URL to a context definition file. -The context definition file is a JSON formatted file that contains the name of the context, the interval in seconds at which +The context definition file is a JSON formatted file that contains the name of the context, the interval (in seconds) at which the context definition file should be monitored, and a list of classpath resources. The LocalCachingContextClassLoaderFactory creates the LocalCachingContext based on the initial contents of the context definition file, and updates the classloader as changes are noticed based on the monitoring interval. An example of the context definition file is below. @@ -47,16 +47,43 @@ 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. + +## Updating a ContextDefinition file + +The LocalCachingContextClassLoaderFactory uses a background thread to fetch the context definition file at the +specified interval. Users can change the context name, monitor interval, and list of resources. Changes to the +context name are ignored however as the context cache directory is created using the context name upon initial +creation. The LocalCachingContextClassLoaderFactory will schedule the next download the of the context +definition file based on the updated monitor interval, and if the list of resources have changed, then they will +be downloaded, verified against their checksums, and used to construct a new ClassLoader for the context. + +## Local Caching + The system property `accumulo.classloader.cache.dir` is required to be set to a local directory on the host. The LocalCachingContext creates a directory at this location for each named context. Each context cache directory contains a lock file and a copy of each fetched resource that is named in the context definition file using the format: `fileName_checksum`. The lock file is used with Java's `FileChannel.tryLock` to enable exclusive access (on supported platforms) to the directory from different processes on the same host. +## Error Handling + +If there is an exception in creating the initial classloader, then a ContextClassLoaderException is thrown. If there is +an exception when updating the classloader, then the exception is logged and the classloader is not updated. Calls +to `LocalCachingContextClassLoaderFactory.getClassLoader(String)` will return the most recent classloader +with valid contents. If the checksum of a downloaded resource does not match the checksum in the context definition +file, then the downloaded version of the file is deleted from the context cache directory so that it can be retried +at the next interval. + ## 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. It is left to the user to remove unused context cache directories and unused old files within a context cache directory. +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. ## Accumulo Configuration diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index a84afd2..b96e0e7 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -87,11 +87,6 @@ hadoop-client-api provided - - org.apache.hadoop - hadoop-client-runtime - provided - org.slf4j 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 f73f243..759d11f 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 @@ -33,7 +33,7 @@ public class Constants { public static final Gson GSON = new GsonBuilder().disableJdkUnsafe().create(); public static DigestUtils getChecksummer() { - return new DigestUtils("MD5"); + 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 8392efa..6c68166 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 @@ -145,7 +145,8 @@ private ClassPathElement cacheResource(final Resource resource) retry.logCompletion(LOG, "Resource " + source.getURL() + " cached locally as " + finalCacheLocation); } catch (IOException e) { - LOG.error("Error copying resource from {}. Retrying...", source.getURL(), e); + LOG.error("Error copying resource from {} to {}. Retrying...", source.getURL(), + finalCacheLocation, e); retry.logRetry(LOG, "Unable to cache resource " + source.getURL()); retry.waitForNextAttempt(LOG, "Cache resource " + source.getURL()); } finally { 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 9631226..e66b3c7 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 @@ -31,7 +31,6 @@ import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import org.apache.accumulo.classloader.lcc.cache.CacheUtils; import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; @@ -109,7 +108,7 @@ private void monitorContext(final String contextLocation, int interval) { // context has been removed from the map, no need to check for update return; } - final AtomicInteger nextInterval = new AtomicInteger(interval); + int nextInterval = interval; final ContextDefinition currentDef = classLoader.getDefinition(); try { final URL contextLocationUrl = new URL(contextLocation); @@ -123,7 +122,7 @@ private void monitorContext(final String contextLocation, int interval) { update.getContextName()); } classLoader.update(update); - nextInterval.set(update.getMonitorIntervalSeconds()); + nextInterval = update.getMonitorIntervalSeconds(); } else { LOG.trace("Context definition for {} has not changed", contextLocation); } @@ -132,7 +131,7 @@ private void monitorContext(final String contextLocation, int interval) { LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", contextLocation, e); } finally { - monitorContext(contextLocation, nextInterval.get()); + monitorContext(contextLocation, nextInterval); } }, interval, TimeUnit.SECONDS); LOG.trace("Monitoring context definition file {} for changes at {} second intervals", diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index 3d952f2..28993e4 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -18,8 +18,6 @@ */ package org.apache.accumulo.classloader.lcc.cache; -import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; -import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java index dfa45d6..cc105b4 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java @@ -18,9 +18,10 @@ */ package org.apache.accumulo.classloader.lcc.resolvers; +import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -28,7 +29,6 @@ import java.nio.file.Path; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import org.apache.hadoop.shaded.org.apache.commons.io.FileUtils; public final class LocalFileResolver extends FileResolver { @@ -58,7 +58,7 @@ public String getFileName() { } @Override - public FileInputStream getInputStream() throws IOException { - return FileUtils.openInputStream(file); + public InputStream getInputStream() throws IOException { + return new BufferedInputStream(Files.newInputStream(file.toPath())); } } 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 7f4b40f..12ece03 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 @@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.File; import java.net.MalformedURLException; @@ -380,7 +381,7 @@ public void testUpdateSameClassNameDifferentContent() throws Exception { @SuppressWarnings("unchecked") Class clazz = - (Class) cl2.loadClass("test.TestObjectA"); + (Class) cl2.loadClass(classA.getClassName()); test.Test impl = clazz.getDeclaredConstructor().newInstance(); assertEquals("Hello from E", impl.hello()); testClassFailsToLoad(cl2, classB); @@ -638,25 +639,29 @@ public void testChangingContext() throws Exception { } else { assertNotEquals(cl, updatedClassLoader); for (URL u : updatedList) { - if (u.equals(jarAOrigLocation)) { + if (u.toString().equals(jarAOrigLocation.toString())) { testClassLoads(updatedClassLoader, classA); - } else if (u.equals(jarBOrigLocation)) { + } else if (u.toString().equals(jarBOrigLocation.toString())) { testClassLoads(updatedClassLoader, classB); - } else if (u.equals(jarCOrigLocation)) { + } else if (u.toString().equals(jarCOrigLocation.toString())) { testClassLoads(updatedClassLoader, classC); - } else if (u.equals(jarDOrigLocation)) { + } else if (u.toString().equals(jarDOrigLocation.toString())) { testClassLoads(updatedClassLoader, classD); + } else { + fail("Unexpected url: " + u.toString()); } } } - if (removed.equals(jarAOrigLocation)) { + if (removed.toString().equals(jarAOrigLocation.toString())) { testClassFailsToLoad(updatedClassLoader, classA); - } else if (removed.equals(jarBOrigLocation)) { + } else if (removed.toString().equals(jarBOrigLocation.toString())) { testClassFailsToLoad(updatedClassLoader, classB); - } else if (removed.equals(jarCOrigLocation)) { + } else if (removed.toString().equals(jarCOrigLocation.toString())) { testClassFailsToLoad(updatedClassLoader, classC); - } else if (removed.equals(jarDOrigLocation)) { + } else if (removed.toString().equals(jarDOrigLocation.toString())) { testClassFailsToLoad(updatedClassLoader, classD); + } else { + fail("Unexpected url: " + removed.toString()); } priorCL = updatedClassLoader; priorList = updatedList; From ea0501b0fafbb6d0ede09995e392ca6e454d4934 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 21 Nov 2025 17:54:46 +0000 Subject: [PATCH 23/31] Changed directory from system property to property passed via init --- modules/local-caching-classloader/README.md | 5 +- .../accumulo/classloader/lcc/Constants.java | 4 +- .../classloader/lcc/LocalCachingContext.java | 4 +- ...LocalCachingContextClassLoaderFactory.java | 25 ++++++++-- .../classloader/lcc/cache/CacheUtils.java | 16 +++---- ...lCachingContextClassLoaderFactoryTest.java | 11 +++-- .../lcc/LocalCachingContextTest.java | 11 ++--- .../classloader/lcc/cache/CacheUtilsTest.java | 46 +++++++------------ 8 files changed, 65 insertions(+), 57 deletions(-) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index 48dc487..66d3116 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -64,7 +64,7 @@ be downloaded, verified against their checksums, and used to construct a new Cla ## Local Caching -The system property `accumulo.classloader.cache.dir` is required to be set to a local directory on the host. The +The property `general.custom.lcc.classloader.cache.dir` is required to be set to a local directory on the host. The LocalCachingContext creates a directory at this location for each named context. Each context cache directory contains a lock file and a copy of each fetched resource that is named in the context definition file using the format: `fileName_checksum`. The lock file is used with Java's `FileChannel.tryLock` to enable exclusive access (on supported @@ -89,7 +89,8 @@ and unused old files within a context cache directory. To use this with Accumulo: - 1. Set the following Accumulo site property: `general.context.class.loader.factory=org.apache.accumulo.classloader.lcc.LocalCachingContextClassLoaderFactory` + 1. Set the following Accumulo site properties: `general.context.class.loader.factory=org.apache.accumulo.classloader.lcc.LocalCachingContextClassLoaderFactory` +`general.custom.lcc.classloader.cache.dir=file://path/to/some/directory` 2. Set the following table property: `table.class.loader.context=(file|hdfs|http|https)://path/to/context/definition.json` 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 759d11f..56bbe1e 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 @@ -21,6 +21,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import org.apache.accumulo.core.conf.Property; import org.apache.commons.codec.digest.DigestUtils; import com.google.gson.Gson; @@ -28,7 +29,8 @@ public class Constants { - public static final String CACHE_DIR_PROPERTY = "accumulo.classloader.cache.dir"; + public static final String CACHE_DIR_PROPERTY = + Property.GENERAL_ARBITRARY_PROP_PREFIX + "lcc.classloader.cache.dir"; public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(0); public static final Gson GSON = new GsonBuilder().disableJdkUnsafe().create(); 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 6c68166..43184f4 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 @@ -114,11 +114,11 @@ public String toString() { .retryAfter(1, TimeUnit.SECONDS).incrementBy(1, TimeUnit.SECONDS).maxWait(5, TimeUnit.MINUTES) .backOffFactor(2).logInterval(1, TimeUnit.SECONDS).createFactory(); - public LocalCachingContext(ContextDefinition contextDefinition) + public LocalCachingContext(final String baseCacheDir, final ContextDefinition contextDefinition) throws IOException, ContextClassLoaderException { this.definition.set(requireNonNull(contextDefinition, "definition must be supplied")); this.contextName = this.definition.get().getContextName(); - this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(contextName); + this.contextCacheDir = CacheUtils.createOrGetContextCacheDir(baseCacheDir, contextName); } public ContextDefinition getDefinition() { 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 e66b3c7..68d0c05 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 @@ -36,12 +36,14 @@ import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.classloader.lcc.definition.Resource; import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; +import org.apache.accumulo.core.spi.common.ContextClassLoaderEnvironment; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.base.Preconditions; /** * A ContextClassLoaderFactory implementation that creates and maintains a ClassLoader for a named @@ -55,9 +57,10 @@ *

* As this class processes the ContextDefinition it fetches the contents of the resource from the * resource URL and caches it in a directory on the local filesystem. This class uses the value of - * the system property {@value Constants#CACHE_DIR_PROPERTY} as the root directory and creates a - * sub-directory for each context name. Each context cache directory contains a lock file and a copy - * of each fetched resource that is named using the following format: fileName_checksum. + * the property {@value Constants#CACHE_DIR_PROPERTY} passed via + * {@link #init(ContextClassLoaderEnvironment)} as the root directory and creates a sub-directory + * for each context name. Each context cache directory contains a lock file and a copy of each + * fetched resource that is named using the following format: fileName_checksum. *

* The lock file prevents processes from manipulating the contexts of the context cache directory * concurrently, which enables the cache directories to be shared among multiple processes on the @@ -76,6 +79,8 @@ public class LocalCachingContextClassLoaderFactory implements ContextClassLoader private final Cache contexts = Caffeine.newBuilder().expireAfterAccess(24, TimeUnit.HOURS).build(); + private volatile String baseCacheDir; + private ContextDefinition parseContextDefinition(final URL url) throws ContextClassLoaderException { LOG.trace("Retrieving context definition file from {}", url); @@ -146,18 +151,28 @@ void resetForTests() { contexts.cleanUp(); } + @Override + public void init(ContextClassLoaderEnvironment env) { + baseCacheDir = requireNonNull(env.getConfiguration().get(Constants.CACHE_DIR_PROPERTY)); + try { + CacheUtils.createBaseCacheDir(baseCacheDir); + } catch (IOException | ContextClassLoaderException e) { + throw new IllegalStateException("Error creating base cache directory at " + baseCacheDir, e); + } + } + @Override public ClassLoader getClassLoader(final String contextLocation) throws ContextClassLoaderException { + Preconditions.checkState(baseCacheDir != null, "init not called before calling getClassLoader"); requireNonNull(contextLocation, "context name must be supplied"); try { final URL contextLocationUrl = new URL(contextLocation); final AtomicBoolean newlyCreated = new AtomicBoolean(false); final LocalCachingContext ccl = contexts.get(contextLocation, cn -> { try { - CacheUtils.createBaseCacheDir(); ContextDefinition def = parseContextDefinition(contextLocationUrl); - LocalCachingContext newCcl = new LocalCachingContext(def); + LocalCachingContext newCcl = new LocalCachingContext(baseCacheDir, def); newCcl.initialize(); newlyCreated.set(true); return newCcl; diff --git a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java index 28993e4..e50cbbc 100644 --- a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java +++ b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/cache/CacheUtils.java @@ -38,7 +38,6 @@ import java.util.EnumSet; import java.util.Set; -import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; public class CacheUtils { @@ -82,18 +81,17 @@ private static Path mkdir(final Path p) throws IOException { } } - public static Path createBaseCacheDir() throws IOException, ContextClassLoaderException { - final String prop = Constants.CACHE_DIR_PROPERTY; - final String cacheDir = System.getProperty(prop); - if (cacheDir == null) { - throw new ContextClassLoaderException("System property " + prop + " not set."); + public static Path createBaseCacheDir(final String baseCacheDir) + throws IOException, ContextClassLoaderException { + if (baseCacheDir == null) { + throw new ContextClassLoaderException("received null for cache directory"); } - return mkdir(Path.of(URI.create(cacheDir))); + return mkdir(Path.of(URI.create(baseCacheDir))); } - public static Path createOrGetContextCacheDir(final String contextName) + public static Path createOrGetContextCacheDir(final String baseCacheDir, final String contextName) throws IOException, ContextClassLoaderException { - Path baseContextDir = createBaseCacheDir(); + Path baseContextDir = createBaseCacheDir(baseCacheDir); return mkdir(baseContextDir.resolve(contextName)); } 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 12ece03..e321aac 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 @@ -38,12 +38,15 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.TreeSet; import org.apache.accumulo.classloader.lcc.TestUtils.TestClassInfo; import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.classloader.lcc.definition.Resource; +import org.apache.accumulo.core.conf.ConfigurationCopy; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; +import org.apache.accumulo.core.util.ConfigurationImpl; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; import org.apache.hadoop.fs.Path; @@ -81,8 +84,11 @@ public class LocalCachingContextClassLoaderFactoryTest { @BeforeAll public static void beforeAll() throws Exception { - String tmp = tempDir.resolve("base").toUri().toString(); - System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); + String baseCacheDir = tempDir.resolve("base").toUri().toString(); + + ConfigurationCopy acuConf = + new ConfigurationCopy(Map.of(Constants.CACHE_DIR_PROPERTY, baseCacheDir)); + FACTORY.init(() -> new ConfigurationImpl(acuConf)); // Find the Test jar files jarAOrigLocation = @@ -145,7 +151,6 @@ public static void beforeAll() throws Exception { @AfterAll public static void afterAll() throws Exception { - System.clearProperty(Constants.CACHE_DIR_PROPERTY); if (jetty != null) { jetty.stop(); jetty.join(); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java index e2bb0ec..b79f516 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextTest.java @@ -54,14 +54,14 @@ public class LocalCachingContextTest { private static TestClassInfo classB; private static TestClassInfo classC; private static TestClassInfo classD; + private static String baseCacheDir; @TempDir private static java.nio.file.Path tempDir; @BeforeAll public static void beforeAll() throws Exception { - String tmp = tempDir.resolve("base").toUri().toString(); - System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); + baseCacheDir = tempDir.resolve("base").toUri().toString(); // Find the Test jar files final URL jarAOrigLocation = @@ -108,7 +108,6 @@ public static void beforeAll() throws Exception { @AfterAll public static void afterAll() throws Exception { - System.clearProperty(Constants.CACHE_DIR_PROPERTY); if (jetty != null) { jetty.stop(); jetty.join(); @@ -120,7 +119,7 @@ public static void afterAll() throws Exception { @Test public void testInitialize() throws Exception { - LocalCachingContext lcccl = new LocalCachingContext(def); + LocalCachingContext lcccl = new LocalCachingContext(baseCacheDir, def); lcccl.initialize(); // Confirm the 3 jars are cached locally @@ -137,7 +136,7 @@ public void testInitialize() throws Exception { @Test public void testClassLoader() throws Exception { - LocalCachingContext lcccl = new LocalCachingContext(def); + LocalCachingContext lcccl = new LocalCachingContext(baseCacheDir, def); lcccl.initialize(); ClassLoader contextClassLoader = lcccl.getClassloader(); @@ -149,7 +148,7 @@ public void testClassLoader() throws Exception { @Test public void testUpdate() throws Exception { - LocalCachingContext lcccl = new LocalCachingContext(def); + LocalCachingContext lcccl = new LocalCachingContext(baseCacheDir, def); lcccl.initialize(); final ClassLoader contextClassLoader = lcccl.getClassloader(); diff --git a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java index 2f1eb86..d928f4e 100644 --- a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/cache/CacheUtilsTest.java @@ -29,42 +29,30 @@ import java.nio.file.Path; import java.util.concurrent.atomic.AtomicReference; -import org.apache.accumulo.classloader.lcc.Constants; import org.apache.accumulo.classloader.lcc.cache.CacheUtils.LockInfo; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +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; public class CacheUtilsTest { - private static final Logger LOG = LoggerFactory.getLogger(CacheUtilsTest.class); - @TempDir private static Path tempDir; - @BeforeEach - public void beforeEach() { - String tmp = tempDir.resolve("base").toUri().toString(); - LOG.info("Setting cache base directory to {}", tmp); - System.setProperty(Constants.CACHE_DIR_PROPERTY, tmp); - } + private static String baseCacheDir = null; - @AfterEach - public void afterEach() { - System.clearProperty(Constants.CACHE_DIR_PROPERTY); + @BeforeAll + public static void beforeAll() throws Exception { + baseCacheDir = tempDir.resolve("base").toUri().toString(); } @Test public void testPropertyNotSet() { - System.clearProperty(Constants.CACHE_DIR_PROPERTY); ContextClassLoaderException ex = - assertThrows(ContextClassLoaderException.class, () -> CacheUtils.createBaseCacheDir()); - assertEquals("Error getting classloader for context: System property " - + Constants.CACHE_DIR_PROPERTY + " not set.", ex.getMessage()); + assertThrows(ContextClassLoaderException.class, () -> CacheUtils.createBaseCacheDir(null)); + assertEquals("Error getting classloader for context: received null for cache directory", + ex.getMessage()); } @Test @@ -72,7 +60,7 @@ public void testCreateBaseDir() throws Exception { final Path base = Path.of(tempDir.resolve("base").toUri()); try { assertFalse(Files.exists(base)); - CacheUtils.createBaseCacheDir(); + CacheUtils.createBaseCacheDir(baseCacheDir); assertTrue(Files.exists(base)); } finally { Files.delete(base); @@ -84,10 +72,10 @@ public void testCreateBaseDirMultipleTimes() throws Exception { final Path base = Path.of(tempDir.resolve("base").toUri()); try { assertFalse(Files.exists(base)); - CacheUtils.createBaseCacheDir(); - CacheUtils.createBaseCacheDir(); - CacheUtils.createBaseCacheDir(); - CacheUtils.createBaseCacheDir(); + CacheUtils.createBaseCacheDir(baseCacheDir); + CacheUtils.createBaseCacheDir(baseCacheDir); + CacheUtils.createBaseCacheDir(baseCacheDir); + CacheUtils.createBaseCacheDir(baseCacheDir); assertTrue(Files.exists(base)); } finally { Files.delete(base); @@ -99,13 +87,13 @@ public void createOrGetContextCacheDir() throws Exception { final Path base = Path.of(tempDir.resolve("base").toUri()); try { assertFalse(Files.exists(base)); - CacheUtils.createOrGetContextCacheDir("context1"); + CacheUtils.createOrGetContextCacheDir(baseCacheDir, "context1"); assertTrue(Files.exists(base)); assertTrue(Files.exists(base.resolve("context1"))); - CacheUtils.createOrGetContextCacheDir("context2"); + CacheUtils.createOrGetContextCacheDir(baseCacheDir, "context2"); assertTrue(Files.exists(base)); assertTrue(Files.exists(base.resolve("context2"))); - CacheUtils.createOrGetContextCacheDir("context1"); + CacheUtils.createOrGetContextCacheDir(baseCacheDir, "context1"); assertTrue(Files.exists(base)); assertTrue(Files.exists(base.resolve("context1"))); } finally { @@ -121,7 +109,7 @@ public void testLock() throws Exception { final Path cx1 = base.resolve("context1"); try { assertFalse(Files.exists(base)); - CacheUtils.createOrGetContextCacheDir("context1"); + CacheUtils.createOrGetContextCacheDir(baseCacheDir, "context1"); assertTrue(Files.exists(base)); assertTrue(Files.exists(cx1)); From e5b78cb6073cd4c85ab39ee8415b33b00bc658dd Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 21 Nov 2025 18:07:49 +0000 Subject: [PATCH 24/31] fix javadoc --- .../classloader/lcc/LocalCachingContextClassLoaderFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 68d0c05..fc1588c 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 @@ -57,7 +57,7 @@ *

* As this class processes the ContextDefinition it fetches the contents of the resource from the * resource URL and caches it in a directory on the local filesystem. This class uses the value of - * the property {@value Constants#CACHE_DIR_PROPERTY} passed via + * the property {@link Constants#CACHE_DIR_PROPERTY} passed via * {@link #init(ContextClassLoaderEnvironment)} as the root directory and creates a sub-directory * for each context name. Each context cache directory contains a lock file and a copy of each * fetched resource that is named using the following format: fileName_checksum. From 1d7a96905b2d6dd2a52e842634fcf17bce4bb2ac Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Mon, 24 Nov 2025 19:23:18 +0000 Subject: [PATCH 25/31] Updates from PR suggestions --- .../accumulo/classloader/lcc/Constants.java | 6 +- ...LocalCachingContextClassLoaderFactory.java | 41 ++++++++++++- ...lCachingContextClassLoaderFactoryTest.java | 60 +++++++++++++++++++ 3 files changed, 104 insertions(+), 3 deletions(-) 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 56bbe1e..414eb46 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 @@ -30,7 +30,11 @@ public class Constants { public static final String CACHE_DIR_PROPERTY = - Property.GENERAL_ARBITRARY_PROP_PREFIX + "lcc.classloader.cache.dir"; + Property.GENERAL_ARBITRARY_PROP_PREFIX + "classloader.lcc.cache.dir"; + + public static final String UPDATE_FAILURE_GRACE_PERIOD_MINS = + Property.GENERAL_ARBITRARY_PROP_PREFIX + "classloader.lcc.update.grace.minutes"; + public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(0); public static final Gson GSON = new GsonBuilder().disableJdkUnsafe().create(); 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 fc1588c..2ecd479 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 @@ -28,7 +28,10 @@ import java.net.URISyntaxException; import java.net.URL; import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -38,6 +41,7 @@ import org.apache.accumulo.classloader.lcc.resolvers.FileResolver; import org.apache.accumulo.core.spi.common.ContextClassLoaderEnvironment; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; +import org.apache.accumulo.core.util.Timer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,7 +83,9 @@ public class LocalCachingContextClassLoaderFactory implements ContextClassLoader private final Cache contexts = Caffeine.newBuilder().expireAfterAccess(24, TimeUnit.HOURS).build(); + private final Map classloaderFailures = new HashMap<>(); private volatile String baseCacheDir; + private volatile Duration updateFailureGracePeriodMins; private ContextDefinition parseContextDefinition(final URL url) throws ContextClassLoaderException { @@ -108,9 +114,12 @@ private ContextDefinition parseContextDefinition(final URL url) */ private void monitorContext(final String contextLocation, int interval) { Constants.EXECUTOR.schedule(() -> { - final LocalCachingContext classLoader = contexts.getIfPresent(contextLocation); + final LocalCachingContext classLoader = + contexts.policy().getIfPresentQuietly(contextLocation); if (classLoader == null) { // context has been removed from the map, no need to check for update + LOG.debug("ClassLoader for context {} not present, no longer monitoring for changes", + contextLocation); return; } int nextInterval = interval; @@ -128,6 +137,7 @@ private void monitorContext(final String contextLocation, int interval) { } classLoader.update(update); nextInterval = update.getMonitorIntervalSeconds(); + classloaderFailures.remove(contextLocation); } else { LOG.trace("Context definition for {} has not changed", contextLocation); } @@ -135,6 +145,29 @@ private void monitorContext(final String contextLocation, int interval) { | NoSuchAlgorithmException | URISyntaxException e) { LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", contextLocation, e); + final Timer failureTimer = classloaderFailures.get(contextLocation); + if (updateFailureGracePeriodMins.isZero()) { + // failure monitoring is disabled + LOG.debug("Property {} not set, not tracking classloader failures for context {}", + Constants.UPDATE_FAILURE_GRACE_PERIOD_MINS, contextLocation); + } else if (failureTimer == null) { + // first failure, start the timer + classloaderFailures.put(contextLocation, Timer.startNew()); + LOG.debug( + "Tracking classloader failures for context {}, will NOT return working classloader if failures continue for {} minutes", + contextLocation, updateFailureGracePeriodMins.toMinutes()); + } else if (failureTimer.hasElapsed(updateFailureGracePeriodMins)) { + // has been failing for the grace period + // unset the classloader reference so that the failure + // will return from getClassLoader in the calling thread + LOG.info("Grace period for failing classloader has elapsed for context {}", + contextLocation); + contexts.invalidate(contextLocation); + classloaderFailures.remove(contextLocation); + } else { + // failing, but grace period has not elapsed. + // No need to log anything. + } } finally { monitorContext(contextLocation, nextInterval); } @@ -153,7 +186,11 @@ void resetForTests() { @Override public void init(ContextClassLoaderEnvironment env) { - baseCacheDir = requireNonNull(env.getConfiguration().get(Constants.CACHE_DIR_PROPERTY)); + baseCacheDir = requireNonNull(env.getConfiguration().get(Constants.CACHE_DIR_PROPERTY), + "Property " + Constants.CACHE_DIR_PROPERTY + " not set, cannot create cache directory."); + String graceProp = env.getConfiguration().get(Constants.UPDATE_FAILURE_GRACE_PERIOD_MINS); + long graceMins = graceProp == null ? 0 : Long.parseLong(graceProp); + updateFailureGracePeriodMins = Duration.ofMinutes(graceMins); try { CacheUtils.createBaseCacheDir(baseCacheDir); } catch (IOException | ContextClassLoaderException e) { 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 e321aac..380c500 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 @@ -671,6 +671,66 @@ public void testChangingContext() throws Exception { priorCL = updatedClassLoader; priorList = updatedList; } + } + + @Test + public void testGracePeriod() throws Exception { + final LocalCachingContextClassLoaderFactory localFactory = + new LocalCachingContextClassLoaderFactory(); + + String baseCacheDir = tempDir.resolve("base").toUri().toString(); + ConfigurationCopy acuConf = new ConfigurationCopy(Map.of(Constants.CACHE_DIR_PROPERTY, + baseCacheDir, Constants.UPDATE_FAILURE_GRACE_PERIOD_MINS, "1")); + localFactory.init(() -> new ConfigurationImpl(acuConf)); + + final ContextDefinition def = + ContextDefinition.create("update", MONITOR_INTERVAL_SECS, jarAOrigLocation); + final Path defFilePath = + createContextDefinitionFile(fs, "UpdateNonExistentResource.json", def.toJson()); + final URL updateDefUrl = new URL(fs.getUri().toString() + defFilePath.toUri().toString()); + + final ClassLoader cl = localFactory.getClassLoader(updateDefUrl.toString()); + + testClassLoads(cl, classA); + testClassFailsToLoad(cl, classB); + testClassFailsToLoad(cl, classC); + testClassFailsToLoad(cl, classD); + + // copy jarA to jarACopy + // create a ContextDefinition that references it + // delete jarACopy + java.nio.file.Path jarAPath = java.nio.file.Path.of(jarAOrigLocation.toURI()); + java.nio.file.Path jarAPathParent = jarAPath.getParent(); + assertNotNull(jarAPathParent); + java.nio.file.Path jarACopy = jarAPathParent.resolve("jarACopy.jar"); + assertTrue(!Files.exists(jarACopy)); + Files.copy(jarAPath, jarACopy, StandardCopyOption.REPLACE_EXISTING); + assertTrue(Files.exists(jarACopy)); + ContextDefinition def2 = + ContextDefinition.create("initial", MONITOR_INTERVAL_SECS, jarACopy.toUri().toURL()); + Files.delete(jarACopy); + assertTrue(!Files.exists(jarACopy)); + + updateContextDefinitionFile(fs, defFilePath, def2.toJson()); + + // wait 2x the monitor interval + Thread.sleep(MONITOR_INTERVAL_SECS * 2 * 1000); + + final ClassLoader cl2 = localFactory.getClassLoader(updateDefUrl.toString()); + + // validate that the classloader has not updated + assertEquals(cl, cl2); + testClassLoads(cl2, classA); + testClassFailsToLoad(cl2, classB); + testClassFailsToLoad(cl2, classC); + testClassFailsToLoad(cl2, classD); + + // Wait 2 minutes for grace period to expire + Thread.sleep(120_000); + + ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, + () -> localFactory.getClassLoader(updateDefUrl.toString())); + assertTrue(ex.getMessage().endsWith("jarACopy.jar does not exist.")); } From 396d76fb3a3309348a36349b5e0f2d53d6c9333a Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Mon, 24 Nov 2025 21:29:41 +0000 Subject: [PATCH 26/31] Updated README for new update grace period property --- modules/local-caching-classloader/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index 66d3116..124f044 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -64,7 +64,7 @@ be downloaded, verified against their checksums, and used to construct a new Cla ## Local Caching -The property `general.custom.lcc.classloader.cache.dir` is required to be set to a local directory on the host. The +The property `general.custom.classloader.lcc.cache.dir` is required to be set to a local directory on the host. The LocalCachingContext creates a directory at this location for each named context. Each context cache directory contains a lock file and a copy of each fetched resource that is named in the context definition file using the format: `fileName_checksum`. The lock file is used with Java's `FileChannel.tryLock` to enable exclusive access (on supported @@ -79,6 +79,13 @@ with valid contents. If the checksum of a downloaded resource does not match the file, then the downloaded version of the file is deleted from the context cache directory so that it can be retried at the next interval. +The property `general.custom.classloader.lcc.update.grace.minutes` determines how long the update process +continues to return the most recent valid classloader when an exception occurs in the background update thread. +A zero value (default) will cause the most recent valid classloader to be returned. Otherwise, the update thread +will fail for N minutes, then clear the reference to the classloader internally. This will cause a subsequent +call to `LocalCachingContextClassLoaderFactory.getClassLoader(String)` to act like the initial call to +create the classloader and return the exception to the calling code. + ## Cleanup Because the cache directory is shared among multiple processes, and one process can't know what the other processes are doing, From b2ab523c2ad5f284bac019b2cc86ba4b53ad58c0 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Tue, 2 Dec 2025 16:41:32 +0000 Subject: [PATCH 27/31] Added test with Mini Accumulo and multiple tservers --- modules/local-caching-classloader/README.md | 2 +- modules/local-caching-classloader/pom.xml | 40 +++ .../accumulo/classloader/lcc/Constants.java | 4 +- ...LocalCachingContextClassLoaderFactory.java | 6 +- ...lCachingContextClassLoaderFactoryTest.java | 2 +- ...AccumuloClusterClassLoaderFactoryTest.java | 284 ++++++++++++++++++ .../src/test/resources/log4j2-test.properties | 11 +- pom.xml | 2 +- 8 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java diff --git a/modules/local-caching-classloader/README.md b/modules/local-caching-classloader/README.md index 124f044..f0f6d2e 100644 --- a/modules/local-caching-classloader/README.md +++ b/modules/local-caching-classloader/README.md @@ -97,7 +97,7 @@ and unused old files within a context cache directory. To use this with Accumulo: 1. Set the following Accumulo site properties: `general.context.class.loader.factory=org.apache.accumulo.classloader.lcc.LocalCachingContextClassLoaderFactory` -`general.custom.lcc.classloader.cache.dir=file://path/to/some/directory` +`general.custom.classloader.lcc.cache.dir=file://path/to/some/directory` 2. Set the following table property: `table.class.loader.context=(file|hdfs|http|https)://path/to/context/definition.json` diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index b96e0e7..cf06eb1 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -93,6 +93,11 @@ slf4j-api provided + + org.apache.accumulo + accumulo-test + test + org.apache.hadoop hadoop-client-minicluster @@ -121,6 +126,41 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-test-jars + + copy + + process-test-resources + + + + org.apache.accumulo + example-iterators-a + ${project.version} + jar + true + ${project.build.directory}/test-classes/ExampleIteratorsA + example-iterators-a.jar + + + org.apache.accumulo + example-iterators-b + ${project.version} + jar + true + ${project.build.directory}/test-classes/ExampleIteratorsB + example-iterators-b.jar + + + + + + org.apache.maven.plugins maven-surefire-plugin 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 414eb46..de0a49f 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 @@ -30,10 +30,10 @@ public class Constants { public static final String CACHE_DIR_PROPERTY = - Property.GENERAL_ARBITRARY_PROP_PREFIX + "classloader.lcc.cache.dir"; + Property.GENERAL_ARBITRARY_PROP_PREFIX.getKey() + "classloader.lcc.cache.dir"; public static final String UPDATE_FAILURE_GRACE_PERIOD_MINS = - Property.GENERAL_ARBITRARY_PROP_PREFIX + "classloader.lcc.update.grace.minutes"; + 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(); 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 2ecd479..14e7c32 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 @@ -142,7 +142,7 @@ private void monitorContext(final String contextLocation, int interval) { LOG.trace("Context definition for {} has not changed", contextLocation); } } catch (ContextClassLoaderException | InterruptedException | IOException - | NoSuchAlgorithmException | URISyntaxException e) { + | NoSuchAlgorithmException | URISyntaxException | RuntimeException e) { LOG.error("Error parsing updated context definition at {}. Classloader NOT updated!", contextLocation, e); final Timer failureTimer = classloaderFailures.get(contextLocation); @@ -165,8 +165,8 @@ private void monitorContext(final String contextLocation, int interval) { contexts.invalidate(contextLocation); classloaderFailures.remove(contextLocation); } else { - // failing, but grace period has not elapsed. - // No need to log anything. + LOG.trace("Failing to update classloader for context {} within the grace period", + contextLocation, e); } } finally { monitorContext(contextLocation, nextInterval); 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 380c500..587de84 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 @@ -62,7 +62,7 @@ public class LocalCachingContextClassLoaderFactoryTest { private static final LocalCachingContextClassLoaderFactory FACTORY = new LocalCachingContextClassLoaderFactory(); - private static final int MONITOR_INTERVAL_SECS = 5; + protected static final int MONITOR_INTERVAL_SECS = 5; private static MiniDFSCluster hdfs; private static FileSystem fs; private static Server jetty; 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 new file mode 100644 index 0000000..5fff345 --- /dev/null +++ b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java @@ -0,0 +1,284 @@ +/* + * 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 static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; +import org.apache.accumulo.core.client.Accumulo; +import org.apache.accumulo.core.client.AccumuloClient; +import org.apache.accumulo.core.client.IteratorSetting; +import org.apache.accumulo.core.client.Scanner; +import org.apache.accumulo.core.clientImpl.AccumuloServerException; +import org.apache.accumulo.core.clientImpl.ClientContext; +import org.apache.accumulo.core.conf.Property; +import org.apache.accumulo.core.data.Key; +import org.apache.accumulo.core.data.TableId; +import org.apache.accumulo.core.data.Value; +import org.apache.accumulo.core.iterators.IteratorUtil.IteratorScope; +import org.apache.accumulo.core.metadata.schema.Ample; +import org.apache.accumulo.core.metadata.schema.TabletMetadata; +import org.apache.accumulo.core.metadata.schema.TabletsMetadata; +import org.apache.accumulo.harness.MiniClusterConfigurationCallback; +import org.apache.accumulo.harness.SharedMiniClusterBase; +import org.apache.accumulo.miniclusterImpl.MiniAccumuloConfigImpl; +import org.apache.accumulo.test.TestIngest; +import org.apache.accumulo.test.TestIngest.IngestParams; +import org.apache.accumulo.test.VerifyIngest; +import org.apache.accumulo.test.VerifyIngest.VerifyParams; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class MiniAccumuloClusterClassLoaderFactoryTest extends SharedMiniClusterBase { + + private static class TestMACConfiguration implements MiniClusterConfigurationCallback { + + @Override + public void configureMiniCluster(MiniAccumuloConfigImpl cfg, + org.apache.hadoop.conf.Configuration coreSite) { + cfg.setNumTservers(3); + cfg.setProperty(Property.TSERV_NATIVEMAP_ENABLED.getKey(), "false"); + cfg.setProperty(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(), + LocalCachingContextClassLoaderFactory.class.getName()); + cfg.setProperty(Constants.CACHE_DIR_PROPERTY, tempDir.resolve("base").toUri().toString()); + cfg.setProperty(Constants.UPDATE_FAILURE_GRACE_PERIOD_MINS, "1"); + } + } + + @TempDir + private static java.nio.file.Path tempDir; + + private static final Set CACHE_DIR_PERMS = + EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); + private static final FileAttribute> PERMISSIONS = + PosixFilePermissions.asFileAttribute(CACHE_DIR_PERMS); + private static final String ITER_CLASS_NAME = + "org.apache.accumulo.classloader.vfs.examples.ExampleIterator"; + private static final int MONITOR_INTERVAL_SECS = + LocalCachingContextClassLoaderFactoryTest.MONITOR_INTERVAL_SECS; + + private static URL jarAOrigLocation; + private static URL jarBOrigLocation; + + @BeforeAll + public static void beforeAll() throws Exception { + + // Find the Test jar files + jarAOrigLocation = MiniAccumuloClusterClassLoaderFactoryTest.class + .getResource("/ExampleIteratorsA/example-iterators-a.jar"); + assertNotNull(jarAOrigLocation); + jarBOrigLocation = MiniAccumuloClusterClassLoaderFactoryTest.class + .getResource("/ExampleIteratorsB/example-iterators-b.jar"); + assertNotNull(jarBOrigLocation); + + startMiniClusterWithConfig(new TestMACConfiguration()); + } + + @AfterAll + public static void afterAll() throws Exception { + stopMiniCluster(); + } + + @Test + public void testClassLoader() throws Exception { + + Path baseDirPath = tempDir.resolve("base"); + 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); + final String testContextDefJson = testContextDef.toJson(); + final File testContextDefFile = jsonDirPath.resolve("testContextDefinition.json").toFile(); + Files.writeString(testContextDefFile.toPath(), testContextDefJson, StandardOpenOption.CREATE); + assertTrue(Files.exists(testContextDefFile.toPath())); + + final String[] names = this.getUniqueNames(1); + try (AccumuloClient client = + Accumulo.newClient().from(getCluster().getClientProperties()).build()) { + + List tservers = client.instanceOperations().getTabletServers(); + Collections.sort(tservers); + assertEquals(3, tservers.size()); + + final String tableName = names[0]; + + final IngestParams params = new IngestParams(client.properties(), tableName, 100); + params.cols = 10; + params.dataSize = 10; + params.startRow = 0; + params.columnFamily = "test"; + params.createTable = true; + params.numsplits = 3; + params.flushAfterRows = 0; + + TestIngest.createTable(client, params); + + // Confirm 4 tablets, spread across 3 tablet servers + client.instanceOperations().waitForBalance(); + + final List tm = getLocations(((ClientContext) client).getAmple(), + client.tableOperations().tableIdMap().get(tableName)); + assertEquals(4, tm.size()); // 3 tablets + + final Set tabletLocations = new TreeSet<>(); + tm.forEach(t -> tabletLocations.add(t.getLocation().getHostPort())); + assertEquals(3, tabletLocations.size()); // 3 locations + + // both collections are sorted + assertIterableEquals(tservers, tabletLocations); + + TestIngest.ingest(client, params); + + final VerifyParams vp = new VerifyParams(client.properties(), tableName, params.rows); + vp.cols = params.cols; + vp.rows = params.rows; + vp.dataSize = params.dataSize; + vp.startRow = params.startRow; + vp.columnFamily = params.columnFamily; + vp.cols = params.cols; + VerifyIngest.verifyIngest(client, vp); + + // Set the table classloader context. Context name is the URL to the context + // definition file + final String contextURL = testContextDefFile.toURI().toURL().toString(); + client.tableOperations().setProperty(tableName, Property.TABLE_CLASSLOADER_CONTEXT.getKey(), + contextURL); + + // Attach a scan iterator to the table + IteratorSetting is = new IteratorSetting(101, "example", ITER_CLASS_NAME); + client.tableOperations().attachIterator(tableName, is, EnumSet.of(IteratorScope.scan)); + + // confirm that all values get transformed to "foo" + // by the iterator + final byte[] jarAValueBytes = "foo".getBytes(UTF_8); + Scanner scanner = client.createScanner(tableName); + int count = 0; + for (Entry e : scanner) { + assertArrayEquals(jarAValueBytes, e.getValue().get()); + count++; + } + assertEquals(1000, count); + + // Update the context definition to point to jar B + final ContextDefinition testContextDefUpdate = + ContextDefinition.create("test", MONITOR_INTERVAL_SECS, jarBOrigLocation); + final String testContextDefUpdateJson = testContextDefUpdate.toJson(); + Files.writeString(testContextDefFile.toPath(), testContextDefUpdateJson, + StandardOpenOption.TRUNCATE_EXISTING); + assertTrue(Files.exists(testContextDefFile.toPath())); + + // Wait 2x the monitor interval + Thread.sleep(2 * MONITOR_INTERVAL_SECS * 1000); + + // Rescan with same iterator class name + // confirm that all values get transformed to "bar" + // by the iterator + final byte[] jarBValueBytes = "bar".getBytes(UTF_8); + scanner = client.createScanner(tableName); + count = 0; + for (Entry e : scanner) { + assertArrayEquals(jarBValueBytes, e.getValue().get()); + count++; + } + assertEquals(1000, count); + + // Copy jar A, create a context definition using the copy, then + // remove the copy so that it's not found when the context classloader + // updates. + java.nio.file.Path jarAPath = java.nio.file.Path.of(jarAOrigLocation.toURI()); + java.nio.file.Path jarAPathParent = jarAPath.getParent(); + assertNotNull(jarAPathParent); + java.nio.file.Path jarACopy = jarAPathParent.resolve("jarACopy.jar"); + assertTrue(!Files.exists(jarACopy)); + Files.copy(jarAPath, jarACopy, StandardCopyOption.REPLACE_EXISTING); + assertTrue(Files.exists(jarACopy)); + + final ContextDefinition testContextDefUpdate2 = + ContextDefinition.create("test", MONITOR_INTERVAL_SECS, jarACopy.toUri().toURL()); + Files.delete(jarACopy); + assertTrue(!Files.exists(jarACopy)); + + final String testContextDefUpdateJson2 = testContextDefUpdate2.toJson(); + Files.writeString(testContextDefFile.toPath(), testContextDefUpdateJson2, + StandardOpenOption.TRUNCATE_EXISTING); + assertTrue(Files.exists(testContextDefFile.toPath())); + + // Wait 2x the monitor interval + Thread.sleep(2 * MONITOR_INTERVAL_SECS * 1000); + + // Rescan and confirm that all values get transformed to "bar" + // by the iterator. The previous class is still being used after + // the monitor interval because the jar referenced does not exist. + scanner = client.createScanner(tableName); + count = 0; + for (Entry e : scanner) { + assertArrayEquals(jarBValueBytes, e.getValue().get()); + count++; + } + assertEquals(1000, count); + + // Wait 2 minutes, 2 times the UPDATE_FAILURE_GRACE_PERIOD_MINS + Thread.sleep(120_000); + + // Scan of table with iterator setting should now fail. + final Scanner scanner2 = client.createScanner(tableName); + RuntimeException re = + assertThrows(RuntimeException.class, () -> scanner2.iterator().hasNext()); + Throwable cause = re.getCause(); + assertTrue(cause instanceof AccumuloServerException); + } + } + + private static List getLocations(Ample ample, String tableId) { + try (TabletsMetadata tabletsMetadata = ample.readTablets().forTable(TableId.of(tableId)) + .fetch(TabletMetadata.ColumnType.LOCATION).build()) { + return tabletsMetadata.stream().collect(Collectors.toList()); + } + } +} diff --git a/modules/local-caching-classloader/src/test/resources/log4j2-test.properties b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties index 3d5abd6..46e83a6 100644 --- a/modules/local-caching-classloader/src/test/resources/log4j2-test.properties +++ b/modules/local-caching-classloader/src/test/resources/log4j2-test.properties @@ -27,11 +27,14 @@ appender.console.target = SYSTEM_OUT appender.console.layout.type = PatternLayout appender.console.layout.pattern = %d{ISO8601} [%-8c{2}] %-5p: %m%n -logger.01.name = org.apache.accumulo.classloader.lcc -logger.01.level = trace +logger.01.name = org.apache.accumulo +logger.01.level = debug -logger.02.name = org.apache.hadoop -logger.02.level = error +logger.02.name = org.apache.accumulo.classloader.lcc +logger.02.level = trace + +logger.03.name = org.apache.hadoop +logger.03.level = error rootLogger.level = warn rootLogger.appenderRef.console.ref = STDOUT diff --git a/pom.xml b/pom.xml index 23a88d1..125cb8f 100644 --- a/pom.xml +++ b/pom.xml @@ -73,10 +73,10 @@ - modules/local-caching-classloader modules/example-iterators-a modules/example-iterators-b modules/vfs-class-loader + modules/local-caching-classloader scm:git:https://gitbox.apache.org/repos/asf/accumulo-classloaders.git From 4fa5aacb5b09d2baccdb7beae8866ed87ba1d889 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Tue, 2 Dec 2025 17:02:25 +0000 Subject: [PATCH 28/31] Add missing minicluster dependency --- modules/local-caching-classloader/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/local-caching-classloader/pom.xml b/modules/local-caching-classloader/pom.xml index cf06eb1..8b39c34 100644 --- a/modules/local-caching-classloader/pom.xml +++ b/modules/local-caching-classloader/pom.xml @@ -93,6 +93,11 @@ slf4j-api provided + + org.apache.accumulo + accumulo-minicluster + test + org.apache.accumulo accumulo-test From 2f6363941171778fc6a1f57a7716186084af15f2 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Tue, 2 Dec 2025 17:35:12 +0000 Subject: [PATCH 29/31] Updates based on PR suggestions --- ...AccumuloClusterClassLoaderFactoryTest.java | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 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 5fff345..001fa77 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 @@ -24,6 +24,7 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -38,6 +39,7 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; @@ -49,8 +51,11 @@ import org.apache.accumulo.classloader.lcc.definition.ContextDefinition; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; +import org.apache.accumulo.core.client.AccumuloException; +import org.apache.accumulo.core.client.AccumuloSecurityException; import org.apache.accumulo.core.client.IteratorSetting; import org.apache.accumulo.core.client.Scanner; +import org.apache.accumulo.core.client.TableNotFoundException; import org.apache.accumulo.core.clientImpl.AccumuloServerException; import org.apache.accumulo.core.clientImpl.ClientContext; import org.apache.accumulo.core.conf.Property; @@ -190,20 +195,27 @@ public void testClassLoader() throws Exception { client.tableOperations().setProperty(tableName, Property.TABLE_CLASSLOADER_CONTEXT.getKey(), contextURL); + // check that the table is returning unique values + // before applying the iterator + final byte[] jarAValueBytes = "foo".getBytes(UTF_8); + List values = getValues(client, tableName); + assertEquals(1000, values.size()); + assertFalse(values.contains(jarAValueBytes)); + // Attach a scan iterator to the table IteratorSetting is = new IteratorSetting(101, "example", ITER_CLASS_NAME); client.tableOperations().attachIterator(tableName, is, EnumSet.of(IteratorScope.scan)); // confirm that all values get transformed to "foo" // by the iterator - final byte[] jarAValueBytes = "foo".getBytes(UTF_8); - Scanner scanner = client.createScanner(tableName); int count = 0; - for (Entry e : scanner) { - assertArrayEquals(jarAValueBytes, e.getValue().get()); - count++; + while (count != 1000) { + try { + count = countExpectedRows(client, tableName, jarAValueBytes); + } catch (AssertionError e) { + // Table not ready, try again + } } - assertEquals(1000, count); // Update the context definition to point to jar B final ContextDefinition testContextDefUpdate = @@ -220,21 +232,15 @@ public void testClassLoader() throws Exception { // confirm that all values get transformed to "bar" // by the iterator final byte[] jarBValueBytes = "bar".getBytes(UTF_8); - scanner = client.createScanner(tableName); - count = 0; - for (Entry e : scanner) { - assertArrayEquals(jarBValueBytes, e.getValue().get()); - count++; - } - assertEquals(1000, count); + assertEquals(1000, countExpectedRows(client, tableName, jarBValueBytes)); // Copy jar A, create a context definition using the copy, then // remove the copy so that it's not found when the context classloader // updates. - java.nio.file.Path jarAPath = java.nio.file.Path.of(jarAOrigLocation.toURI()); - java.nio.file.Path jarAPathParent = jarAPath.getParent(); + Path jarAPath = Path.of(jarAOrigLocation.toURI()); + Path jarAPathParent = jarAPath.getParent(); assertNotNull(jarAPathParent); - java.nio.file.Path jarACopy = jarAPathParent.resolve("jarACopy.jar"); + Path jarACopy = jarAPathParent.resolve("jarACopy.jar"); assertTrue(!Files.exists(jarACopy)); Files.copy(jarAPath, jarACopy, StandardCopyOption.REPLACE_EXISTING); assertTrue(Files.exists(jarACopy)); @@ -255,13 +261,7 @@ public void testClassLoader() throws Exception { // Rescan and confirm that all values get transformed to "bar" // by the iterator. The previous class is still being used after // the monitor interval because the jar referenced does not exist. - scanner = client.createScanner(tableName); - count = 0; - for (Entry e : scanner) { - assertArrayEquals(jarBValueBytes, e.getValue().get()); - count++; - } - assertEquals(1000, count); + assertEquals(1000, countExpectedRows(client, tableName, jarBValueBytes)); // Wait 2 minutes, 2 times the UPDATE_FAILURE_GRACE_PERIOD_MINS Thread.sleep(120_000); @@ -275,6 +275,25 @@ public void testClassLoader() throws Exception { } } + private List getValues(AccumuloClient client, String table) + throws TableNotFoundException, AccumuloSecurityException, AccumuloException { + Scanner scanner = client.createScanner(table); + List values = new ArrayList<>(1000); + scanner.forEach((k, v) -> values.add(v.get())); + return values; + } + + private int countExpectedRows(AccumuloClient client, String table, byte[] expectedValue) + throws TableNotFoundException, AccumuloSecurityException, AccumuloException { + Scanner scanner = client.createScanner(table); + int count = 0; + for (Entry e : scanner) { + assertArrayEquals(expectedValue, e.getValue().get()); + count++; + } + return count; + } + private static List getLocations(Ample ample, String tableId) { try (TabletsMetadata tabletsMetadata = ample.readTablets().forTable(TableId.of(tableId)) .fetch(TabletMetadata.ColumnType.LOCATION).build()) { From 911ac1625d0024674cf01b683e9b0344e2e6ea21 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Tue, 2 Dec 2025 18:00:19 +0000 Subject: [PATCH 30/31] Updated with PR suggestion --- ...AccumuloClusterClassLoaderFactoryTest.java | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 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 001fa77..0e64cc2 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 @@ -22,9 +22,7 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -39,7 +37,7 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; @@ -198,9 +196,7 @@ public void testClassLoader() throws Exception { // check that the table is returning unique values // before applying the iterator final byte[] jarAValueBytes = "foo".getBytes(UTF_8); - List values = getValues(client, tableName); - assertEquals(1000, values.size()); - assertFalse(values.contains(jarAValueBytes)); + assertEquals(0, countExpectedRows(client, tableName, jarAValueBytes)); // Attach a scan iterator to the table IteratorSetting is = new IteratorSetting(101, "example", ITER_CLASS_NAME); @@ -210,11 +206,7 @@ public void testClassLoader() throws Exception { // by the iterator int count = 0; while (count != 1000) { - try { - count = countExpectedRows(client, tableName, jarAValueBytes); - } catch (AssertionError e) { - // Table not ready, try again - } + count = countExpectedRows(client, tableName, jarAValueBytes); } // Update the context definition to point to jar B @@ -275,21 +267,14 @@ public void testClassLoader() throws Exception { } } - private List getValues(AccumuloClient client, String table) - throws TableNotFoundException, AccumuloSecurityException, AccumuloException { - Scanner scanner = client.createScanner(table); - List values = new ArrayList<>(1000); - scanner.forEach((k, v) -> values.add(v.get())); - return values; - } - private int countExpectedRows(AccumuloClient client, String table, byte[] expectedValue) throws TableNotFoundException, AccumuloSecurityException, AccumuloException { Scanner scanner = client.createScanner(table); int count = 0; for (Entry e : scanner) { - assertArrayEquals(expectedValue, e.getValue().get()); - count++; + if (Arrays.equals(e.getValue().get(), expectedValue)) { + count++; + } } return count; } From b167057335915eefac647dcefb2f23a588d1828f Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Tue, 2 Dec 2025 18:29:18 +0000 Subject: [PATCH 31/31] Rename test method --- .../lcc/MiniAccumuloClusterClassLoaderFactoryTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 0e64cc2..6e6fed2 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 @@ -196,7 +196,7 @@ public void testClassLoader() throws Exception { // check that the table is returning unique values // before applying the iterator final byte[] jarAValueBytes = "foo".getBytes(UTF_8); - assertEquals(0, countExpectedRows(client, tableName, jarAValueBytes)); + assertEquals(0, countExpectedValues(client, tableName, jarAValueBytes)); // Attach a scan iterator to the table IteratorSetting is = new IteratorSetting(101, "example", ITER_CLASS_NAME); @@ -206,7 +206,7 @@ public void testClassLoader() throws Exception { // by the iterator int count = 0; while (count != 1000) { - count = countExpectedRows(client, tableName, jarAValueBytes); + count = countExpectedValues(client, tableName, jarAValueBytes); } // Update the context definition to point to jar B @@ -224,7 +224,7 @@ public void testClassLoader() throws Exception { // confirm that all values get transformed to "bar" // by the iterator final byte[] jarBValueBytes = "bar".getBytes(UTF_8); - assertEquals(1000, countExpectedRows(client, tableName, jarBValueBytes)); + assertEquals(1000, countExpectedValues(client, tableName, jarBValueBytes)); // Copy jar A, create a context definition using the copy, then // remove the copy so that it's not found when the context classloader @@ -253,7 +253,7 @@ public void testClassLoader() throws Exception { // Rescan and confirm that all values get transformed to "bar" // by the iterator. The previous class is still being used after // the monitor interval because the jar referenced does not exist. - assertEquals(1000, countExpectedRows(client, tableName, jarBValueBytes)); + assertEquals(1000, countExpectedValues(client, tableName, jarBValueBytes)); // Wait 2 minutes, 2 times the UPDATE_FAILURE_GRACE_PERIOD_MINS Thread.sleep(120_000); @@ -267,7 +267,7 @@ public void testClassLoader() throws Exception { } } - private int countExpectedRows(AccumuloClient client, String table, byte[] expectedValue) + private int countExpectedValues(AccumuloClient client, String table, byte[] expectedValue) throws TableNotFoundException, AccumuloSecurityException, AccumuloException { Scanner scanner = client.createScanner(table); int count = 0;