This is an automated email from the ASF dual-hosted git repository.
ctubbsii pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo-classloaders.git
The following commit(s) were added to refs/heads/main by this push:
new 39afc71 Create init utility (#69)
39afc71 is described below
commit 39afc71d52458474c0ccdffee996f2cd2bee66c4
Author: Christopher Tubbs <[email protected]>
AuthorDate: Mon Feb 9 15:26:01 2026 -0500
Create init utility (#69)
* Move CLI commands into separate package
* Minor refactor to allow LocalStore to be initialized and to store
without creating a classloader to avoid utility from leaving behind
unnecessary working directories
* Ensure monitor threads are daemon threads (observed that they held up
process exit before refactoring LocalStore to be used without creating
a classloader; now unneeded, but still a good change)
---
.../classloader/ccl/CachingClassLoaderFactory.java | 47 ++++--------
.../accumulo/classloader/ccl/LocalStore.java | 39 ++++++++--
.../classloader/ccl/cli/CreateManifest.java | 73 +++++++++++++++++++
.../apache/accumulo/classloader/ccl/cli/Help.java | 53 ++++++++++++++
.../classloader/ccl/cli/InitializeCache.java | 80 +++++++++++++++++++++
.../classloader/ccl/manifest/Manifest.java | 83 +---------------------
.../accumulo/classloader/ccl/LocalStoreTest.java | 3 +-
7 files changed, 258 insertions(+), 120 deletions(-)
diff --git
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactory.java
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactory.java
index d5b199b..70db567 100644
---
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactory.java
+++
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactory.java
@@ -20,18 +20,14 @@ package org.apache.accumulo.classloader.ccl;
import static java.util.Objects.requireNonNull;
-import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.ref.Cleaner;
import java.net.MalformedURLException;
-import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
@@ -39,6 +35,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
@@ -113,8 +110,15 @@ public class CachingClassLoaderFactory implements
ContextClassLoaderFactory {
private static final Logger LOG =
LoggerFactory.getLogger(CachingClassLoaderFactory.class);
private static final Cleaner CLEANER = Cleaner.create();
+ private static final AtomicLong monitorThreadCounter = new AtomicLong(0);
+
// executor for the monitor tasks
- private final ScheduledExecutorService executor =
Executors.newScheduledThreadPool(0);
+ private final ScheduledExecutorService executor =
Executors.newScheduledThreadPool(0, r -> {
+ var t = new Thread(r);
+ t.setName("url-context-monitor-thread-" +
monitorThreadCounter.getAndIncrement());
+ t.setDaemon(true);
+ return t;
+ });
// stores the latest seen manifest for a remote URL; String types are used
here for the key
// instead of URL because URL.hashCode could trigger network activity for
hostname lookups
@@ -157,7 +161,7 @@ public class CachingClassLoaderFactory implements
ContextClassLoaderFactory {
@Override
public void init(ContextClassLoaderEnvironment env) {
- String value = requireNonNull(env.getConfiguration().get(PROP_CACHE_DIR),
+ String baseDir = requireNonNull(env.getConfiguration().get(PROP_CACHE_DIR),
"Property " + PROP_CACHE_DIR + " not set, cannot create cache
directory.");
// these suppliers are used so we can update these config properties
without restarting,
@@ -190,25 +194,10 @@ public class CachingClassLoaderFactory implements
ContextClassLoaderFactory {
+ " No ClassLoader instances will be created until it is set.",
PROP_ALLOWED_URLS, env.getConfiguration().get(PROP_ALLOWED_URLS),
npe);
}
- final Path baseCacheDir;
- if (value.startsWith("file:")) {
- try {
- baseCacheDir = Path.of(new URL(value).toURI());
- } catch (IOException | URISyntaxException e) {
- throw new IllegalArgumentException(
- "Malformed file: URL specified for base directory: " + value, e);
- }
- } else if (value.startsWith("/")) {
- baseCacheDir = Path.of(value);
- } else {
- throw new IllegalArgumentException(
- "Base directory is neither a file URL nor an absolute file path: " +
value);
- }
try {
- localStore.set(new LocalStore(baseCacheDir, allowedUrlChecker));
+ localStore.set(new LocalStore(baseDir, allowedUrlChecker));
} catch (IOException e) {
- throw new UncheckedIOException("Unable to create the local storage area
at " + baseCacheDir,
- e);
+ throw new UncheckedIOException("Unable to create the local storage area
at " + baseDir, e);
}
}
@@ -358,7 +347,7 @@ public class CachingClassLoaderFactory implements
ContextClassLoaderFactory {
"Exception creating a hard link in {} due to missing resource {};
attempting re-download of context resources",
failedHardLinksDir, e.getMissingResource(), e);
try {
- recursiveDelete(failedHardLinksDir);
+ LocalStore.recursiveDelete(failedHardLinksDir);
} catch (IOException ioe) {
LOG.warn(
"Saw exception removing directory {} after hard link creation
failure; this should be cleaned up manually",
@@ -385,7 +374,7 @@ public class CachingClassLoaderFactory implements
ContextClassLoaderFactory {
final var cleanDir = hardLinksDir;
CLEANER.register(cl, () -> {
try {
- recursiveDelete(cleanDir);
+ LocalStore.recursiveDelete(cleanDir);
} catch (IOException e) {
LOG.warn("Saw exception when executing cleaner on directory {}",
cleanDir, e);
}
@@ -393,12 +382,4 @@ public class CachingClassLoaderFactory implements
ContextClassLoaderFactory {
return cl;
}
- private static void recursiveDelete(Path directory) throws IOException {
- if (Files.exists(directory)) {
- try (var walker = Files.walk(directory)) {
-
walker.map(Path::toFile).sorted(Comparator.reverseOrder()).forEach(File::delete);
- }
- }
- }
-
}
diff --git
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/LocalStore.java
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/LocalStore.java
index be65060..ec14ab2 100644
---
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/LocalStore.java
+++
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/LocalStore.java
@@ -27,14 +27,17 @@ import static java.util.Objects.requireNonNull;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
+import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.util.Comparator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
@@ -85,7 +88,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
* their checksums are verified before use, so that a {@link URLClassLoader}
can be constructed
* using the same resource ordering as in the {@link Manifest} file.
*/
-final class LocalStore {
+public final class LocalStore {
private static final Logger LOG = LoggerFactory.getLogger(LocalStore.class);
private static final String PID =
Long.toString(ProcessHandle.current().pid());
@@ -98,7 +101,27 @@ final class LocalStore {
public static final String RESOURCES_DIR = "resources";
public static final String WORKING_DIR = "working";
- public LocalStore(final Path baseDir, final BiConsumer<String,URL>
allowedUrlChecker)
+ public LocalStore(final String baseDir, final BiConsumer<String,URL>
allowedUrlChecker)
+ throws IOException {
+ this(baseDirStringToPath(baseDir), allowedUrlChecker);
+ }
+
+ private static Path baseDirStringToPath(final String value) {
+ if (value.startsWith("file:")) {
+ try {
+ return Path.of(new URL(value).toURI());
+ } catch (IOException | URISyntaxException e) {
+ throw new IllegalArgumentException(
+ "Malformed file: URL specified for base directory: " + value, e);
+ }
+ } else if (value.startsWith("/")) {
+ return Path.of(value);
+ }
+ throw new IllegalArgumentException(
+ "Base directory is neither a file URL nor an absolute file path: " +
value);
+ }
+
+ LocalStore(final Path baseDir, final BiConsumer<String,URL>
allowedUrlChecker)
throws IOException {
requireNonNull(baseDir);
this.allowedUrlChecker = requireNonNull(allowedUrlChecker);
@@ -174,7 +197,7 @@ final class LocalStore {
* Save the {@link Manifest} to the manifests directory, and all of its
resources to the resources
* directory.
*/
- void storeContext(final Manifest manifest) {
+ public void storeContext(final Manifest manifest) {
requireNonNull(manifest, "manifest must be supplied");
final String destinationName = checksumForFileName(manifest) + ".json";
try {
@@ -369,7 +392,7 @@ final class LocalStore {
}
}
- Path createWorkingHardLinks(final Manifest manifest, Consumer<Path>
forEachLink)
+ public Path createWorkingHardLinks(final Manifest manifest, Consumer<Path>
forEachLink)
throws HardLinkFailedException {
Path hardLinkDir = createTempDirectory("context-" +
checksumForFileName(manifest));
// create all hard links first
@@ -419,4 +442,12 @@ final class LocalStore {
}
+ public static void recursiveDelete(Path directory) throws IOException {
+ if (Files.exists(directory)) {
+ try (var walker = Files.walk(directory)) {
+
walker.map(Path::toFile).sorted(Comparator.reverseOrder()).forEach(File::delete);
+ }
+ }
+ }
+
}
diff --git
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/CreateManifest.java
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/CreateManifest.java
new file mode 100644
index 0000000..3441329
--- /dev/null
+++
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/CreateManifest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.ccl.cli;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.accumulo.classloader.ccl.CachingClassLoaderFactory;
+import org.apache.accumulo.classloader.ccl.manifest.Manifest;
+import org.apache.accumulo.start.spi.KeywordExecutable;
+
+import com.beust.jcommander.Parameter;
+import com.google.auto.service.AutoService;
+
+@AutoService(KeywordExecutable.class)
+public class CreateManifest implements KeywordExecutable {
+
+ static class Opts extends Help {
+ @Parameter(names = {"-i", "--interval"}, required = true,
+ description = "monitor interval (in seconds)", arity = 1, order = 1)
+ int monitorInterval;
+
+ @Parameter(names = {"-a", "--algorithm"}, required = false,
+ description = "checksum algorithm to use (default: SHA-512)", arity =
1, order = 2)
+ String algorithm = "SHA-512";
+
+ @Parameter(required = true, description = "classpath element URL (<url>[
<url>...])",
+ arity = -1, order = 3)
+ public List<String> files = new ArrayList<>();
+ }
+
+ public CreateManifest() {}
+
+ @Override
+ public String keyword() {
+ return "create-classloader-manifest";
+ }
+
+ @Override
+ public String description() {
+ return "Creates and prints a class loader context manifest for the "
+ + CachingClassLoaderFactory.class.getSimpleName();
+ }
+
+ @Override
+ public void execute(String[] args) throws Exception {
+ var opts = new Opts();
+ opts.parseArgs(CreateManifest.class.getName(), args);
+ URL[] urls = new URL[opts.files.size()];
+ int count = 0;
+ for (String f : opts.files) {
+ urls[count++] = new URL(f);
+ }
+ System.out.print(Manifest.create(opts.monitorInterval, opts.algorithm,
urls).toJson());
+ }
+}
diff --git
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/Help.java
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/Help.java
new file mode 100644
index 0000000..85ed461
--- /dev/null
+++
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/Help.java
@@ -0,0 +1,53 @@
+/*
+ * 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.ccl.cli;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+
+class Help {
+ @Parameter(names = {"-h", "-?", "--help", "-help"}, help = true)
+ public boolean help = false;
+
+ void parseArgs(String programName, String[] args) {
+ JCommander commander = new JCommander();
+ commander.addObject(this);
+ commander.setProgramName(programName);
+ try {
+ commander.parse(args);
+ } catch (ParameterException ex) {
+ commander.usage();
+ exitWithError(ex.getMessage(), 1);
+ }
+ if (help) {
+ commander.usage();
+ exit(0);
+ }
+ }
+
+ private void exit(int status) {
+ System.exit(status);
+ }
+
+ private void exitWithError(String message, int status) {
+ System.err.println(message);
+ exit(status);
+ }
+}
diff --git
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/InitializeCache.java
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/InitializeCache.java
new file mode 100644
index 0000000..903c502
--- /dev/null
+++
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/cli/InitializeCache.java
@@ -0,0 +1,80 @@
+/*
+ * 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.ccl.cli;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.accumulo.classloader.ccl.CachingClassLoaderFactory;
+import org.apache.accumulo.classloader.ccl.LocalStore;
+import org.apache.accumulo.classloader.ccl.manifest.Manifest;
+import org.apache.accumulo.start.spi.KeywordExecutable;
+
+import com.beust.jcommander.Parameter;
+import com.google.auto.service.AutoService;
+
+@AutoService(KeywordExecutable.class)
+public class InitializeCache implements KeywordExecutable {
+
+ static class Opts extends Help {
+ @Parameter(names = {"-d", "--directory"}, required = true,
+ description = "the local directory to initialize and stage", arity =
1, order = 1)
+ String directory;
+
+ @Parameter(names = {"-v", "--verify"}, required = false,
+ description = "also verify existing files if they were found already
present", arity = 0,
+ order = 2)
+ boolean verify;
+
+ @Parameter(required = false,
+ description = "URLs for context manifests to load (<url>[ <url>...])",
arity = -1,
+ order = 3)
+ public List<String> manifests = new ArrayList<>();
+ }
+
+ public InitializeCache() {}
+
+ @Override
+ public String keyword() {
+ return "init-classloader-cache-dir";
+ }
+
+ @Override
+ public String description() {
+ return "Initializes the specified directory for the "
+ + CachingClassLoaderFactory.class.getSimpleName()
+ + " and stages any resources for the specified context manifest
locations";
+ }
+
+ @Override
+ public void execute(String[] args) throws Exception {
+ var opts = new Opts();
+ opts.parseArgs(InitializeCache.class.getName(), args);
+ var localStore = new LocalStore(opts.directory, (a, b) -> {/* allow all
*/});
+ for (String manifestUrl : opts.manifests) {
+ var manifest = Manifest.download(new URL(manifestUrl));
+ localStore.storeContext(manifest);
+ if (opts.verify) {
+ var workingDir = localStore.createWorkingHardLinks(manifest, p -> {/*
do nothing */});
+ LocalStore.recursiveDelete(workingDir);
+ }
+ }
+ }
+}
diff --git
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
index e3d3672..80a3891 100644
---
a/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
+++
b/modules/caching-class-loader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
@@ -28,22 +28,14 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
-import java.util.List;
import java.util.Objects;
import java.util.Set;
-import java.util.function.Consumer;
import java.util.function.Supplier;
-import org.apache.accumulo.start.spi.KeywordExecutable;
import org.apache.commons.codec.digest.DigestUtils;
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.google.auto.service.AutoService;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import com.google.gson.Gson;
@@ -51,59 +43,7 @@ import com.google.gson.GsonBuilder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-@AutoService(KeywordExecutable.class)
-public class Manifest implements KeywordExecutable {
-
- static class Opts {
- @Parameter(names = {"-i", "--interval"}, required = true,
- description = "monitor interval (in seconds)", arity = 1, order = 1)
- int monitorInterval;
-
- @Parameter(names = {"-a", "--algorithm"}, required = false,
- description = "checksum algorithm to use (default: " + SHA_512 + ")",
arity = 1, order = 2)
- String algorithm = SHA_512;
-
- @Parameter(required = true, description = "classpath element URL (<url>[
<url>...])",
- arity = -1, order = 3)
- public List<String> files = new ArrayList<>();
-
- @Parameter(names = {"-h", "-?", "--help", "-help"}, help = true)
- public boolean help = false;
-
- void parseArgs(Consumer<JCommander> jcConsumer, String programName,
String[] args,
- Object... others) {
- JCommander commander = new JCommander();
- jcConsumer.accept(commander);
- commander.addObject(this);
- for (Object other : others) {
- commander.addObject(other);
- }
- commander.setProgramName(programName);
- try {
- commander.parse(args);
- } catch (ParameterException ex) {
- commander.usage();
- exitWithError(ex.getMessage(), 1);
- }
- if (help) {
- commander.usage();
- exit(0);
- }
- }
-
- void parseArgs(String programName, String[] args, Object... others) {
- parseArgs(jCommander -> {}, programName, args, others);
- }
-
- void exit(int status) {
- System.exit(status);
- }
-
- void exitWithError(String message, int status) {
- System.err.println(message);
- exit(status);
- }
- }
+public class Manifest {
// pretty-print uses Unix newline
private static final Gson GSON =
@@ -200,25 +140,4 @@ public class Manifest implements KeywordExecutable {
return GSON.toJson(this).stripTrailing() + "\n";
}
- @Override
- public String keyword() {
- return "create-classloader-manifest";
- }
-
- @Override
- public String description() {
- return "Creates and prints a class loader context manifest for the
CachingClassLoaderFactory";
- }
-
- @Override
- public void execute(String[] args) throws Exception {
- Opts opts = new Opts();
- opts.parseArgs(Manifest.class.getName(), args);
- URL[] urls = new URL[opts.files.size()];
- int count = 0;
- for (String f : opts.files) {
- urls[count++] = new URL(f);
- }
- System.out.print(create(opts.monitorInterval, opts.algorithm,
urls).toJson());
- }
}
diff --git
a/modules/caching-class-loader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
b/modules/caching-class-loader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
index 22a1cb3..ce050ed 100644
---
a/modules/caching-class-loader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
+++
b/modules/caching-class-loader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
@@ -133,7 +133,8 @@ public class LocalStoreTest {
@Test
public void testPropertyNotSet() {
// test baseDir not set
- assertThrows(NullPointerException.class, () -> new LocalStore(null,
ALLOW_ALL_URLS));
+ assertThrows(NullPointerException.class, () -> new LocalStore((Path) null,
ALLOW_ALL_URLS));
+ assertThrows(NullPointerException.class, () -> new LocalStore((String)
null, ALLOW_ALL_URLS));
// test URL checker not set
assertThrows(NullPointerException.class, () -> new
LocalStore(baseCacheDir, null));
}