diff --git a/docs/groovy/conceptual/execution-context.md b/docs/groovy/conceptual/execution-context.md
index 347d91ac2bb..ca537fa88a3 100644
--- a/docs/groovy/conceptual/execution-context.md
+++ b/docs/groovy/conceptual/execution-context.md
@@ -235,7 +235,7 @@ executionContext = ExecutionContext.newBuilder()
.newQueryScope()
.setOperationInitializer(OperationInitializer.NON_PARALLELIZABLE)
.setUpdateGraph(PeriodicUpdateGraph.newBuilder("MyCustomGraph").build())
- .setQueryCompiler(QueryCompilerImpl.create(Files.createTempDirectory("qc_").toFile(), ClassLoader.getSystemClassLoader()))
+ .setQueryCompiler(QueryCompilerImpl.create())
.build()
```
@@ -248,7 +248,7 @@ This approach allows you to specify:
- **Query scope**: A new, empty query scope via `newQueryScope`.
- **Operation initializer**: Parallelization behavior of operations.
- **Update graph**: A custom update graph (e.g., `PeriodicUpdateGraph` or `EventDrivenUpdateGraph`).
-- **Query compiler**: A compiler instance with a specified working directory and class loader.
+- **Query compiler**: A compiler instance with an optional directory to load classes from and classloader.
For use cases requiring event-driven updates instead of periodic updates, you can substitute an `EventDrivenUpdateGraph`:
@@ -262,7 +262,7 @@ executionContext = ExecutionContext.newBuilder()
.newQueryScope()
.setOperationInitializer(OperationInitializer.NON_PARALLELIZABLE)
.setUpdateGraph(eventDrivenGraph)
- .setQueryCompiler(QueryCompilerImpl.create(Files.createTempDirectory("qc_").toFile(), ClassLoader.getSystemClassLoader()))
+ .setQueryCompiler(QueryCompilerImpl.create())
.build()
```
diff --git a/docs/python/conceptual/execution-context.md b/docs/python/conceptual/execution-context.md
index eb7f69f6c4e..ef50996cda2 100644
--- a/docs/python/conceptual/execution-context.md
+++ b/docs/python/conceptual/execution-context.md
@@ -260,12 +260,7 @@ execution_context = (
.newQueryScope()
.setOperationInitializer(OperationInitializer.NON_PARALLELIZABLE)
.setUpdateGraph(PeriodicUpdateGraph.newBuilder("MyCustomGraph").build())
- .setQueryCompiler(
- QueryCompilerImpl.create(
- jpy.get_type("java.io.File")(temp_dir),
- jpy.get_type("java.lang.ClassLoader").getSystemClassLoader(),
- )
- )
+ .setQueryCompiler(QueryCompilerImpl.create())
.build()
)
```
@@ -279,7 +274,7 @@ This approach allows you to specify:
- **Query scope**: A new, empty query scope via `newQueryScope`.
- **Operation initializer**: Parallelization behavior of operations.
- **Update graph**: A custom update graph (e.g., `PeriodicUpdateGraph` or `EventDrivenUpdateGraph`).
-- **Query compiler**: A compiler instance with a specified working directory and class loader.
+- **Query compiler**: A custom query compiler, which can be created with an optional class directory, plus optional classloader
For use cases requiring event-driven updates instead of periodic updates, you can substitute an `EventDrivenUpdateGraph`:
@@ -306,12 +301,7 @@ execution_context = (
.newQueryScope()
.setOperationInitializer(OperationInitializer.NON_PARALLELIZABLE)
.setUpdateGraph(event_driven_graph)
- .setQueryCompiler(
- QueryCompilerImpl.create(
- jpy.get_type("java.io.File")(temp_dir),
- jpy.get_type("java.lang.ClassLoader").getSystemClassLoader(),
- )
- )
+ .setQueryCompiler(QueryCompilerImpl.create())
.build()
)
```
diff --git a/engine/context/src/main/java/io/deephaven/engine/context/QueryCompiler.java b/engine/context/src/main/java/io/deephaven/engine/context/QueryCompiler.java
index 4041f91cf02..638946bb13e 100644
--- a/engine/context/src/main/java/io/deephaven/engine/context/QueryCompiler.java
+++ b/engine/context/src/main/java/io/deephaven/engine/context/QueryCompiler.java
@@ -10,6 +10,16 @@
import java.util.concurrent.ExecutionException;
+/**
+ * Java compiler api, specifically geared towards query language expressions. Implementations should be constructed with
+ * a path on disk to existing bytecode to use in addition to {@code java.class.path}. Likewise, callers must ensure that
+ * the context classloader already contains those input classes - the new class will be loaded in a child classloader to
+ * ensure that the class can be GCd when neither it nor the compiler instance are reachable.
+ *
+ * Requests for compilation will never depend on other classes compiled by this or any other QueryCompiler, and are
+ * expected to be unique based on the provided class name. This enables the implementation to cache classes and only
+ * compile and load a class once per name.
+ */
public interface QueryCompiler {
/**
diff --git a/engine/context/src/test/java/io/deephaven/engine/context/TestQueryCompiler.java b/engine/context/src/test/java/io/deephaven/engine/context/TestQueryCompiler.java
index fa39ad0211f..11af4585216 100644
--- a/engine/context/src/test/java/io/deephaven/engine/context/TestQueryCompiler.java
+++ b/engine/context/src/test/java/io/deephaven/engine/context/TestQueryCompiler.java
@@ -14,9 +14,7 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import java.io.IOException;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -60,18 +58,14 @@ private static String generateClassBody(final String className) {
@Rule
public final EngineCleanup framework = new EngineCleanup();
- @Rule
- public TemporaryFolder folder = new TemporaryFolder();
-
private SafeCloseable executionContextClosable;
@Before
- public void setUp() throws IOException {
+ public void setUp() {
executionContextClosable = ExecutionContext.newBuilder()
.captureQueryLibrary()
.captureQueryScope()
- .setQueryCompiler(QueryCompilerImpl.create(
- folder.newFolder(), TestQueryCompiler.class.getClassLoader()))
+ .setQueryCompiler(QueryCompilerImpl.create())
.build()
.open();
}
@@ -375,12 +369,15 @@ public void testBadCompile() {
CompletionStageFuture.make(),
};
- final QueryCompilerImpl badCompiler = QueryCompilerImpl.createForUnitTests(List.of("InvalidClassArgument"));
+ final QueryCompilerImpl badCompiler = QueryCompilerImpl.create();
+ badCompiler.setClassNamesForAnnotationProcessing(List.of("InvalidClassArgument"));
+
UncheckedDeephavenException e = org.junit.Assert.assertThrows(UncheckedDeephavenException.class,
() -> badCompiler.compile(requests, resolvers));
org.junit.Assert.assertEquals("Error Invoking Compiler, no source present in diagnostic:\n" +
"Class names, 'InvalidClassArgument', are only accepted if annotation processing is explicitly requested",
e.getMessage());
+
}
@Test
diff --git a/engine/table/src/main/java/io/deephaven/engine/context/QueryCompilerImpl.java b/engine/table/src/main/java/io/deephaven/engine/context/QueryCompilerImpl.java
index 6cce3289372..68ad3a58899 100644
--- a/engine/table/src/main/java/io/deephaven/engine/context/QueryCompilerImpl.java
+++ b/engine/table/src/main/java/io/deephaven/engine/context/QueryCompilerImpl.java
@@ -3,13 +3,13 @@
//
package io.deephaven.engine.context;
+import com.google.common.hash.Hashing;
import io.deephaven.UncheckedDeephavenException;
import io.deephaven.base.FileUtils;
import io.deephaven.base.log.LogOutput;
import io.deephaven.base.log.LogOutputAppendable;
import io.deephaven.base.verify.Assert;
import io.deephaven.configuration.Configuration;
-import io.deephaven.configuration.DataDir;
import io.deephaven.engine.context.util.SynchronizedJavaFileManager;
import io.deephaven.engine.table.impl.perf.BasePerformanceEntry;
import io.deephaven.engine.table.impl.perf.QueryPerformanceRecorder;
@@ -17,39 +17,69 @@
import io.deephaven.engine.table.impl.util.JobScheduler;
import io.deephaven.engine.table.impl.util.OperationInitializerJobScheduler;
import io.deephaven.internal.log.LoggerFactory;
-import io.deephaven.io.log.LogEntry;
import io.deephaven.io.log.impl.LogOutputStringImpl;
import io.deephaven.io.logger.Logger;
-import io.deephaven.util.ByteUtils;
import io.deephaven.util.CompletionStageFuture;
+import io.deephaven.util.annotations.TestUseOnly;
import io.deephaven.util.mutable.MutableInt;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
-
-import javax.tools.*;
-import java.io.*;
+import org.jetbrains.annotations.Nullable;
+
+import javax.tools.Diagnostic;
+import javax.tools.FileObject;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
-import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
-import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.*;
-import java.util.concurrent.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
-import java.util.function.Supplier;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+/**
+ * A {@link QueryCompiler} implementation that compiles Java source to bytecode in memory and defines the resulting
+ * classes via {@link ClassLoader#defineClass}, avoiding filesystem writes for compilation output.
+ *
+ *
+ * The compiler resolves dependencies from {@code java.class.path} and an optional additional classpath directory
+ * (typically where Groovy writes its bytecode). Compiled classes are loaded in per-batch child classloaders of the
+ * provided parent classloader, enabling GC of both classes and classloaders when neither the compiled Classes nor this
+ * compiler instance are reachable.
+ */
public class QueryCompilerImpl implements QueryCompiler, LogOutputAppendable {
private static final Logger log = LoggerFactory.getLogger(QueryCompilerImpl.class);
@@ -59,19 +89,9 @@ public class QueryCompilerImpl implements QueryCompiler, LogOutputAppendable {
private static final int DEFAULT_MAX_STRING_LITERAL_LENGTH = 65500;
private static final String JAVA_CLASS_VERSION = System.getProperty("java.class.version").replace('.', '_');
- private static final int MAX_CLASS_COLLISIONS = 128;
private static final String IDENTIFYING_FIELD_NAME = "_CLASS_BODY_";
- private static final String CODEGEN_TIMEOUT_PROP = "QueryCompiler.codegen.timeoutMs";
- private static final long CODEGEN_TIMEOUT_MS_DEFAULT = TimeUnit.SECONDS.toMillis(10); // 10 seconds
- private static final String CODEGEN_LOOP_DELAY_PROP = "QueryCompiler.codegen.retry.delay";
- private static final long CODEGEN_LOOP_DELAY_MS_DEFAULT = 100;
- private static final long CODEGEN_TIMEOUT_MS =
- Configuration.getInstance().getLongWithDefault(CODEGEN_TIMEOUT_PROP, CODEGEN_TIMEOUT_MS_DEFAULT);
- private static final long CODEGEN_LOOP_DELAY_MS =
- Configuration.getInstance().getLongWithDefault(CODEGEN_LOOP_DELAY_PROP, CODEGEN_LOOP_DELAY_MS_DEFAULT);
-
private static boolean logEnabled = Configuration.getInstance().getBoolean("QueryCompiler.logEnabledDefault");
private static final String TRACE_PREFIX_PROPERTY = "QueryCompiler.tracePrefixes";
@@ -108,7 +128,6 @@ private static boolean shouldTrace(String className) {
return TRACE_INCLUDE_PREFIXES.stream().anyMatch(className::startsWith);
}
-
private static JavaCompiler compiler;
private static final AtomicReference fileManagerCache = new AtomicReference<>();
@@ -147,57 +166,135 @@ private static void releaseFileManager(@NotNull final JavaFileManager fileManage
public static final String FORMULA_CLASS_PREFIX = "io.deephaven.temp";
public static final String DYNAMIC_CLASS_PREFIX = "io.deephaven.dynamic";
- public static QueryCompilerImpl create(File cacheDirectory, ClassLoader classLoader) {
- return new QueryCompilerImpl(cacheDirectory, classLoader, true, null);
+ /**
+ * Creates a new QueryCompilerImpl. Uses the current thread's context classloader as the parent for per-batch
+ * classloaders (as required by {@link QueryCompiler}'s contract).
+ *
+ * @param additionalClassPathDir optional directory to add to the compiler's classpath (e.g., groovy bytecode dir)
+ */
+ public static QueryCompilerImpl create(@Nullable final File additionalClassPathDir) {
+ return create(additionalClassPathDir, null);
}
- static QueryCompilerImpl createForUnitTests() {
- return createForUnitTests(null);
+ /**
+ * Creates a new QueryCompilerImpl. Uses the current thread's context classloader as the parent for per-batch
+ * classloaders (as required by {@link QueryCompiler}'s contract).
+ *
+ * @param additionalClassPathDir optional directory to add to the compiler's classpath (e.g., groovy bytecode dir)
+ */
+ public static QueryCompilerImpl create(@Nullable final File additionalClassPathDir,
+ @Nullable ClassLoader classLoader) {
+ return new QueryCompilerImpl(additionalClassPathDir, classLoader);
}
- static QueryCompilerImpl createForUnitTests(final List classNamesForAnnotationProcessing) {
- final Path queryCompilerDir = DataDir.get()
- .resolve("io.deephaven.engine.context.QueryCompiler.createForUnitTests");
- return new QueryCompilerImpl(queryCompilerDir.toFile(), QueryCompilerImpl.class.getClassLoader(), false,
- classNamesForAnnotationProcessing);
+ /**
+ * Creates a new compiler that has no extra directory to read from, suitable for unit tests or cases where the
+ * existing classpath is sufficient.
+ */
+ public static QueryCompilerImpl create() {
+ return create(null, null);
}
- private final Map>> knownClasses = new HashMap<>();
+ /**
+ * Cache from SHA-256 hash of class body to entry. While compilation is in-flight, the entry holds a future for
+ * coordination. Once complete, the entry transitions to a weak reference so the class (and its classloader and
+ * bytecode) can be GC'd when no longer in use. All transitions are atomic via per-entry synchronization. Stale
+ * entries are cleaned up via a {@link ReferenceQueue} drained at the start of each {@link #compile} call.
+ */
+ private final ConcurrentHashMap cache = new ConcurrentHashMap<>();
- private final String[] dynamicPatterns = new String[] {DYNAMIC_CLASS_PREFIX, FORMULA_CLASS_PREFIX};
+ /** Queue that receives cleared weak references, allowing us to remove stale cache entries. */
+ private final ReferenceQueue> staleQueue = new ReferenceQueue<>();
- private final File classDestination;
- private final Set additionalClassLocations;
- private final WritableURLClassLoader ucl;
// This is for test use only, specifying a non-null list causes an error without a specific source to be generated.
- private final List classNamesForAnnotationProcessing;
+ private List classNamesForAnnotationProcessing;
- private QueryCompilerImpl(
- @NotNull final File classDestination,
- @NotNull final ClassLoader parentClassLoader,
- boolean classDestinationIsAlsoClassSource,
- final List classNamesForAnnotationProcessing) {
- ensureJavaCompiler();
+ /**
+ * A WeakReference that remembers its cache key, so we can remove the entry when the referent is GC'd.
+ */
+ private static final class KeyedWeakReference extends WeakReference> {
+ final String key;
- this.classDestination = classDestination;
- ensureDirectories(this.classDestination, () -> "Failed to create missing class destination directory " +
- classDestination.getAbsolutePath());
- additionalClassLocations = new LinkedHashSet<>();
+ KeyedWeakReference(@NotNull Class> referent, @NotNull String key,
+ @NotNull ReferenceQueue> queue) {
+ super(referent, queue);
+ this.key = key;
+ }
+ }
- URL[] urls = new URL[1];
- try {
- urls[0] = (classDestination.toURI().toURL());
- } catch (MalformedURLException e) {
- throw new UncheckedDeephavenException(e);
+ /**
+ * A cache entry that transitions from in-flight (future-based coordination) to weakly-cached (reclaimable).
+ * Thread-safe: all reads/writes are synchronized on the entry itself. The entry self-transitions to weak-ref state
+ * when its future completes successfully, via a callback registered at construction time.
+ *
+ *
+ * Safety invariant: {@code CompletionStageFutureImpl} (extends {@code CompletableFuture}) retains its result
+ * strongly in an internal field even after listeners fire. This means any thread holding a reference to the future
+ * can still retrieve the class via {@code get()}, even after this entry has transitioned to weak-ref state. The
+ * weak reference only governs the cache's retention — not in-flight callers'.
+ *
+ */
+ private static final class CacheEntry {
+ private CompletionStageFuture> future;
+ private WeakReference> classRef;
+
+ CacheEntry(@NotNull CompletionStageFuture> future,
+ @NotNull String key,
+ @NotNull ReferenceQueue> queue) {
+ this.future = future;
+ // Self-transition: when compilation completes, swap to keyed weak ref
+ future.whenComplete((clazz, err) -> {
+ if (clazz != null) {
+ complete(clazz, key, queue);
+ }
+ // On failure, the entry stays in-flight with a failed future.
+ // The caller will remove it from the cache.
+ });
+ }
+
+ /** Transition to weak-ref state: releases the strong reference held by the completed future. */
+ private synchronized void complete(
+ @NotNull Class> clazz, @NotNull String key, @NotNull ReferenceQueue> queue) {
+ this.classRef = new KeyedWeakReference(clazz, key, queue);
+ this.future = null;
+ }
+
+ /**
+ * Atomically resolve this entry's state. Returns one of:
+ *
+ * - A {@code Class>} — cache hit, class still alive
+ * - A {@code CompletionStageFuture>} — compilation in flight, wait on it
+ * - {@code null} — entry is stale (class was GC'd), caller should retry
+ *
+ */
+ synchronized Optional>> resolve() {
+ if (classRef != null) {
+ final Class> c = classRef.get();
+ return Optional.ofNullable(c).map(CompletableFuture::completedFuture);
+ }
+ return Optional.of(future);
}
- this.ucl = new WritableURLClassLoader(urls, parentClassLoader);
- log.trace().append("Class destination is ").append(classDestination.toString()).endl();
- if (classDestinationIsAlsoClassSource) {
- addClassSource(classDestination);
+ /** True if completed but the class has been GC'd (stale). Used inside compute() lambda. */
+ synchronized boolean isStale() {
+ return future == null && (classRef == null || classRef.get() == null);
}
+ }
- this.classNamesForAnnotationProcessing = classNamesForAnnotationProcessing;
+ /** The context classloader provided at creation time, or null if none set */
+ @Nullable
+ private final ClassLoader parentClassLoader;
+
+ /** Optional additional classpath directory (e.g., where Groovy writes bytecode). */
+ @Nullable
+ private final File additionalClassPathDir;
+
+ private QueryCompilerImpl(
+ @Nullable final File additionalClassPathDir,
+ @Nullable final ClassLoader parentClassLoader) {
+ ensureJavaCompiler();
+ this.additionalClassPathDir = additionalClassPathDir;
+ this.parentClassLoader = parentClassLoader;
if (log.isTraceEnabled()) {
log.trace().append("QueryCompiler Class Path: ").append(getClassPath()).append(File.pathSeparator)
@@ -205,9 +302,29 @@ private QueryCompilerImpl(
}
}
+ /** Drains the stale queue and removes any cache entries whose weak references have been cleared by GC. */
+ private void evictStaleEntries() {
+ KeyedWeakReference ref;
+ while ((ref = (KeyedWeakReference) staleQueue.poll()) != null) {
+ cache.remove(ref.key);
+ }
+ }
+
+ /**
+ * Currently only intended for testing, this method allows specifying annotation processors to run during this
+ * compile.
+ *
+ * @param classNames the annotation processor classes to allow to run
+ */
+ @TestUseOnly
+ public void setClassNamesForAnnotationProcessing(List classNames) {
+ classNamesForAnnotationProcessing = classNames;
+ }
+
@Override
public LogOutput append(LogOutput logOutput) {
- return logOutput.append("QueryCompiler{classDestination=").append(classDestination.getAbsolutePath())
+ return logOutput.append("QueryCompilerImpl{additionalClassPathDir=")
+ .append(additionalClassPathDir == null ? "null" : additionalClassPathDir.getAbsolutePath())
.append("}");
}
@@ -228,78 +345,6 @@ public static boolean setLogEnabled(boolean logEnabled) {
return original;
}
- /*
- * NB: This is (obviously) not thread safe if code tries to write the same className to the same
- * destinationDirectory from multiple threads. Seeing as we don't currently have this use case, leaving
- * synchronization as an external concern.
- */
- public static void writeClass(final File destinationDirectory, final String className, final byte[] data)
- throws IOException {
- writeClass(destinationDirectory, className, data, null);
- }
-
- /*
- * NB: This is (obviously) not thread safe if code tries to write the same className to the same
- * destinationDirectory from multiple threads. Seeing as we don't currently have this use case, leaving
- * synchronization as an external concern.
- */
- public static void writeClass(final File destinationDirectory, final String className, final byte[] data,
- final String message) throws IOException {
- final File destinationFile = new File(destinationDirectory,
- className.replace('.', File.separatorChar) + JavaFileObject.Kind.CLASS.extension);
-
- final boolean shouldTrace = shouldTrace(className);
-
- if (destinationFile.exists()) {
- if (shouldTrace) {
- log.trace().append("Destination class file already exists ").append(destinationFile.toString()).endl();
- }
-
- final byte[] existingBytes = Files.readAllBytes(destinationFile.toPath());
- if (Arrays.equals(existingBytes, data)) {
- if (message == null) {
- log.info().append("Ignoring pushed class ").append(className)
- .append(" because it already exists in this context!").endl();
- } else {
- log.info().append("Ignoring pushed class ").append(className).append(message)
- .append(" because it already exists in this context!").endl();
- }
- return;
- } else {
- if (message == null) {
- log.info().append("Pushed class ").append(className)
- .append(" already exists in this context, but has changed!").endl();
- } else {
- log.info().append("Pushed class ").append(className).append(message)
- .append(" already exists in this context, but has changed!").endl();
- }
- if (!destinationFile.delete()) {
- throw new IOException("Could not delete existing class file: " + destinationFile);
- }
- }
- } else if (shouldTrace) {
- log.trace().append("Destination class file does not already exist ").append(destinationFile.toString())
- .endl();
- }
-
- final File parentDir = destinationFile.getParentFile();
- ensureDirectories(parentDir,
- () -> "Unable to create missing destination directory " + parentDir.getAbsolutePath());
- if (!destinationFile.createNewFile()) {
- throw new UncheckedDeephavenException(
- "Unable to create destination file " + destinationFile.getAbsolutePath());
- }
- final ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(data.length);
- byteOutStream.write(data, 0, data.length);
- final FileOutputStream fileOutStream = new FileOutputStream(destinationFile);
- byteOutStream.writeTo(fileOutStream);
- fileOutStream.close();
-
- if (shouldTrace) {
- log.trace().append("Wrote destination class file ").append(destinationFile.toString()).endl();
- }
- }
-
@Override
public void compile(
@NotNull final QueryCompilerRequest[] requests,
@@ -311,6 +356,9 @@ public void compile(
throw new IllegalArgumentException("Requests and resolvers must be the same length");
}
+ // Opportunistically clean up stale cache entries whose classes have been GC'd
+ evictStaleEntries();
+
final boolean shouldTrace =
log.isTraceEnabled() && Arrays.stream(requests).map(QueryCompilerRequest::packageNameRoot)
.anyMatch(QueryCompilerImpl::shouldTrace);
@@ -327,58 +375,80 @@ public void compile(
}, requests).endl();
}
- // noinspection unchecked
- final CompletionStageFuture>[] allFutures = new CompletionStageFuture[requests.length];
-
+ // For each request: cache hit, in-flight wait, or needs compilation
final List newRequests = new ArrayList<>();
final List>> newResolvers = new ArrayList<>();
+ final List newHashes = new ArrayList<>();
+ final List>> allFutures = new ArrayList<>(requests.length);
- synchronized (this) {
- for (int ii = 0; ii < requests.length; ++ii) {
- final QueryCompilerRequest request = requests[ii];
- final CompletionStageFuture.Resolver> resolver = resolvers[ii];
+ for (int ii = 0; ii < requests.length; ++ii) {
+ // Compute content hash for deduplication
+ QueryCompilerRequest req = requests[ii];
+ String hash = Hashing.sha256().hashString(req.classBody(), StandardCharsets.UTF_8).toString();
+
+ final CompletionStageFuture.Resolver> resolver = resolvers[ii];
+
+ // Resolve cache state, retrying if we hit a stale entry race
+ Optional>> resolved;
+ while (true) {
+ final CacheEntry entry = cache.compute(hash, (key, existing) -> {
+ if (existing != null && !existing.isStale()) {
+ return existing;
+ }
- CompletionStageFuture> future =
- knownClasses.putIfAbsent(request.classBody(), resolver.getFuture());
- if (future == null) {
- newRequests.add(request);
+ // No/stale existing entry, we will do the compilation ourselves
newResolvers.add(resolver);
- future = resolver.getFuture();
- } else if (shouldTrace) {
- log.trace().append("Found existing future in knownClasses for ").append(request.className())
- .append(" (done=").append(future.isDone()).append(")").endl();
+ newRequests.add(req);
+ newHashes.add(hash);
+ return new CacheEntry(resolver.getFuture(), key, staleQueue);
+ });
+
+ resolved = entry.resolve();
+ if (resolved.isPresent()) {
+ // Got a future, either to our work, or someone else's
+ allFutures.add(resolved.get());
+ break;
}
- allFutures[ii] = future;
+ // Stale race: entry transitioned between compute() and resolve(). Remove and retry.
+ cache.remove(hash, entry);
}
}
+ // Compile new requests
if (!newResolvers.isEmpty()) {
- // It's my job to fulfill these futures.
try {
- compileHelper(newRequests, newResolvers);
+ compileHelper(newRequests, newResolvers, newHashes);
} catch (RuntimeException e) {
- // These failures are not applicable to a single request, so we can't just complete the future and
- // leave the failure in the cache.
- synchronized (this) {
- for (int ii = 0; ii < newRequests.size(); ++ii) {
- if (newResolvers.get(ii).completeExceptionally(e)) {
- knownClasses.remove(newRequests.get(ii).classBody());
- }
+ for (int ii = 0; ii < newHashes.size(); ++ii) {
+ if (newResolvers.get(ii).completeExceptionally(e)) {
+ cache.remove(newHashes.get(ii));
}
}
throw e;
}
+
+ // Remove cache entries for failed or incomplete compilations
+ // (successful ones self-transitioned to weak refs via whenComplete)
+ for (int ii = 0; ii < newHashes.size(); ++ii) {
+ try {
+ newResolvers.get(ii).getFuture().get(); // throws if completed exceptionally
+ } catch (Exception ex) {
+ cache.remove(newHashes.get(ii));
+ }
+ }
}
+ // Wait on in-flight futures from other compilations
for (int ii = 0; ii < requests.length; ++ii) {
+ final Future> future = allFutures.get(ii);
try {
- resolvers[ii].complete(allFutures[ii].get());
+ resolvers[ii].complete(future.get());
} catch (ExecutionException err) {
resolvers[ii].completeExceptionally(err.getCause());
} catch (InterruptedException err) {
// This can only occur if we are interrupted while waiting for the future to complete from another
// compilation request.
- Assert.notEquals(resolvers[ii], "resolvers[ii]", allFutures[ii], "allFutures[ii]");
+ Assert.notEquals(resolvers[ii], "resolvers[ii]", allFutures.get(ii), "allFutures.get(ii)");
resolvers[ii].completeExceptionally(err);
} catch (Throwable err) {
resolvers[ii].completeExceptionally(err);
@@ -386,562 +456,365 @@ public void compile(
}
}
- private static void ensureDirectories(final File file, final Supplier runtimeErrMsg) {
- // File.mkdirs() checks for existence on entry, in which case it returns false.
- // It may also return false on a failure to create.
- // Also note, two separate threads or JVMs may be running this code in parallel. It's possible that we could
- // lose the race
- // (and therefore mkdirs() would return false), but still get the directory we need (and therefore exists()
- // would return true)
- if (!file.mkdirs() && !file.isDirectory()) {
- throw new UncheckedDeephavenException(runtimeErrMsg.get());
- }
- }
-
- private ClassLoader getClassLoaderForFormula(final Map> parameterClasses) {
- return new URLClassLoader(ucl.getURLs(), ucl) {
- // Once we find a class that is missing, we should not attempt to load it again,
- // otherwise we can end up with a StackOverflow Exception
- final HashSet missingClasses = new HashSet<>();
-
- @Override
- protected Class> findClass(String name) throws ClassNotFoundException {
- final boolean shouldTrace = shouldTrace(name);
-
- // If we have a parameter that uses this class, return it
- final Class> paramClass = parameterClasses.get(name);
- if (paramClass != null) {
- if (shouldTrace) {
- log.trace().append("findClass(").append(name).append("): matched parameter class").endl();
- }
- return paramClass;
- }
+ private void compileHelper(
+ @NotNull final List requests,
+ @NotNull final List>> resolvers,
+ @NotNull final List hashes) {
+ // Assign unique FQ class names for each request using pre-computed hashes
+ final String[] fqClassNames = new String[requests.size()];
+ final String[] packageNames = new String[requests.size()];
- // Unless we are looking for a formula or Groovy class, we should use the default behavior
- if (!isFormulaClass(name)) {
- if (shouldTrace) {
- log.trace().append("findClass(").append(name)
- .append("): not a formula class, delegating to super.findClass").endl();
- }
- return super.findClass(name);
- }
+ for (int ii = 0; ii < requests.size(); ++ii) {
+ final QueryCompilerRequest request = requests.get(ii);
+ final String packageNameSuffix = "c_" + hashes.get(ii) + "v" + JAVA_CLASS_VERSION;
+ packageNames[ii] = request.getPackageName(packageNameSuffix);
+ fqClassNames[ii] = packageNames[ii] + "." + request.className();
+ }
- // if it is a groovy class, always try to use the instance in the shell
- if (name.startsWith(DYNAMIC_CLASS_PREFIX)) {
- try {
- final Class> dynamicResult = ucl.getParent().loadClass(name);
- if (shouldTrace) {
- log.trace().append("findClass(").append(name)
- .append("): loaded dynamic class from parent of WritableURLClassLoader").endl();
- }
- return dynamicResult;
- } catch (final ClassNotFoundException ignored) {
- if (shouldTrace) {
- log.trace().append("findClass(").append(name)
- .append("): dynamic class not found in parent of WritableURLClassLoader,"
- + " falling through")
- .endl();
- }
- // we'll try to load it otherwise
- }
- }
+ // Build compilation attempts
+ final List attempts = new ArrayList<>();
+ for (int ii = 0; ii < requests.size(); ++ii) {
+ attempts.add(new CompilationRequestAttempt(
+ requests.get(ii), packageNames[ii], fqClassNames[ii], resolvers.get(ii)));
+ }
- // We've already not found this class, so we should not try to search again
- if (missingClasses.contains(name)) {
- if (shouldTrace) {
- log.trace().append("findClass(").append(name)
- .append("): previously cached as missing, delegating to super.findClass").endl();
- }
- return super.findClass(name);
- }
+ // Compile and define
+ compileAndDefine(attempts);
- final byte[] bytes;
- try {
- bytes = loadClassData(name);
- } catch (IOException ioe) {
- if (shouldTrace) {
- log.trace().append("findClass(").append(name)
- .append("): loadClassData failed (").append(ioe.getMessage())
- .append("), caching as missing and delegating to super.loadClass").endl();
- }
- missingClasses.add(name);
- return super.loadClass(name);
- }
- if (shouldTrace) {
- log.trace().append("findClass(").append(name).append("): defining class from ")
- .append(bytes.length).append(" bytes").endl();
- }
- return defineClass(name, bytes, 0, bytes.length);
+ // Validate _CLASS_BODY_ field on successfully defined classes
+ for (int ii = 0; ii < requests.size(); ++ii) {
+ final CompletionStageFuture.Resolver> resolver = resolvers.get(ii);
+ if (resolver.getFuture().isDone()) {
+ continue; // already completed (success or failure)
}
+ // This shouldn't happen - compileAndDefine should have resolved everything
+ resolver.completeExceptionally(new IllegalStateException(
+ "Class was not resolved after compilation: " + fqClassNames[ii]));
+ }
+ }
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- private boolean isFormulaClass(String name) {
- return Arrays.stream(dynamicPatterns).anyMatch(name::startsWith);
- }
+ private void compileAndDefine(@NotNull final List requests) {
+ final ExecutionContext executionContext = ExecutionContext.getContext();
+ final int parallelismFactor = executionContext.getOperationInitializer().parallelismFactor();
- @Override
- public Class> loadClass(String name) throws ClassNotFoundException {
- if (!isFormulaClass(name)) {
- return super.loadClass(name);
- }
- return findClass(name);
- }
+ final int requestsPerTask = Math.max(32, (requests.size() + parallelismFactor - 1) / parallelismFactor);
- private byte[] loadClassData(String name) throws IOException {
- final boolean shouldTrace = shouldTrace(name);
+ final int numTasks;
+ final JobScheduler jobScheduler;
- final File destFile = new File(classDestination,
- name.replace('.', File.separatorChar) + JavaFileObject.Kind.CLASS.extension);
- if (destFile.exists()) {
- if (shouldTrace) {
- log.trace().append("loadClassData(").append(name).append("): found at ")
- .append(destFile.toString()).endl();
- }
- return Files.readAllBytes(destFile.toPath());
- }
- if (shouldTrace) {
- log.trace().append("loadClassData(").append(name).append("): not present at ")
- .append(destFile.toString()).endl();
- }
+ final boolean canParallelize = executionContext.getOperationInitializer().canParallelize();
+ if (!canParallelize || parallelismFactor == 1 || requestsPerTask >= requests.size()) {
+ numTasks = 1;
+ jobScheduler = new ImmediateJobScheduler();
+ } else {
+ numTasks = (requests.size() + requestsPerTask - 1) / requestsPerTask;
+ jobScheduler = new OperationInitializerJobScheduler();
+ }
- for (File location : additionalClassLocations) {
- final File checkFile = new File(location,
- name.replace('.', File.separatorChar) + JavaFileObject.Kind.CLASS.extension);
- if (checkFile.exists()) {
- if (shouldTrace) {
- log.trace().append("loadClassData(").append(name).append("): found at ")
- .append(checkFile.toString()).endl();
- }
- return Files.readAllBytes(checkFile.toPath());
- }
- if (shouldTrace) {
- log.trace().append("loadClassData(").append(name).append("): not present at ")
- .append(checkFile.toString()).endl();
- }
- }
+ log.trace().append("maybeCreateClasses: ").append(requests.size()).append(" requests, ")
+ .append(numTasks).append(" tasks, ").append(requestsPerTask).endl();
- if (shouldTrace) {
- log.trace().append("loadClassData(").append(name)
- .append("): not found in any class location").endl();
- }
- throw new FileNotFoundException(name);
+ final JavaFileManager fileManager = acquireFileManager();
+ final AtomicReference exception = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Runnable cleanup = () -> {
+ try {
+ releaseFileManager(fileManager);
+ } catch (Exception e) {
+ // ignore errors here
+ } finally {
+ latch.countDown();
}
};
- }
-
- private static class WritableURLClassLoader extends URLClassLoader {
- private WritableURLClassLoader(URL[] urls, ClassLoader parent) {
- super(urls, parent);
- }
- @Override
- protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- final boolean shouldTrace = shouldTrace(name);
- final int loaderId = System.identityHashCode(this);
-
- Class> clazz = findLoadedClass(name);
- if (clazz != null) {
- if (shouldTrace) {
- log.trace().append("WritableURLClassLoader@").append(loaderId)
- .append(".loadClass(").append(name).append(") returned from findLoadedClass").endl();
- }
- return clazz;
+ final Consumer onError = err -> {
+ if (err instanceof RuntimeException) {
+ exception.set((RuntimeException) err);
+ } else {
+ exception.set(new UncheckedDeephavenException("Error during compilation", err));
}
+ cleanup.run();
+ };
- try {
- clazz = findClass(name);
- if (shouldTrace) {
- log.trace().append("WritableURLClassLoader@").append(loaderId)
- .append(".loadClass(").append(name).append(") returned from findClass").endl();
- }
- } catch (ClassNotFoundException e) {
- if (getParent() != null) {
- if (shouldTrace) {
- log.trace().append("WritableURLClassLoader@").append(loaderId)
- .append(".loadClass(").append(name).append(") delegating to parent").endl();
- }
- clazz = getParent().loadClass(name);
- if (shouldTrace) {
- log.trace().append("WritableURLClassLoader@").append(loaderId)
- .append(".loadClass(").append(name).append(") parent returned class").endl();
- }
- } else {
- if (shouldTrace) {
- log.trace().append("WritableURLClassLoader@").append(loaderId)
- .append(".loadClass(").append(name)
- .append(") not found by findClass and no parent loader; returning null").endl();
- }
- }
- }
+ jobScheduler.iterateParallel(executionContext, null, JobScheduler.DEFAULT_CONTEXT_FACTORY,
+ 0, numTasks, (context, jobId, nestedErrorConsumer) -> {
+ final int startInclusive = jobId * requestsPerTask;
+ final int endExclusive = Math.min(requests.size(), (jobId + 1) * requestsPerTask);
+ doCompileAndDefine(fileManager, requests, startInclusive, endExclusive);
+ },
+ () -> {
+ },
+ cleanup,
+ onError);
- if (resolve) {
- resolveClass(clazz);
+ try {
+ latch.await();
+ final BasePerformanceEntry perfEntry = jobScheduler.getAccumulatedPerformance();
+ if (perfEntry != null) {
+ QueryPerformanceRecorder.getInstance().getEnclosingNugget().accumulate(perfEntry);
}
- return clazz;
- }
-
- @Override
- public synchronized void addURL(URL url) {
- super.addURL(url);
+ final RuntimeException err = exception.get();
+ if (err != null) {
+ throw err;
+ }
+ } catch (final InterruptedException e) {
+ throw new CancellationException("interrupted while compiling");
}
}
- private void addClassSource(File classSourceDirectory) {
- synchronized (additionalClassLocations) {
- if (additionalClassLocations.contains(classSourceDirectory)) {
- return;
- }
- additionalClassLocations.add(classSourceDirectory);
+ private void doCompileAndDefine(
+ @NotNull final JavaFileManager fileManager,
+ @NotNull final List requests,
+ final int startInclusive,
+ final int endExclusive) {
+ final List toRetry = new ArrayList<>();
+ final boolean wantRetry = doCompileAndDefineSingleRound(
+ fileManager, requests, startInclusive, endExclusive, toRetry);
+ if (!wantRetry) {
+ return;
}
- try {
- ucl.addURL(classSourceDirectory.toURI().toURL());
- } catch (MalformedURLException e) {
- throw new UncheckedDeephavenException(e);
+ // Retry non-failing requests from the first pass
+ final List ignored = new ArrayList<>();
+ if (doCompileAndDefineSingleRound(fileManager, toRetry, 0, toRetry.size(), ignored)) {
+ throw new IllegalStateException("Unexpected failure during second pass of compilation");
}
}
- private File getClassDestination() {
- return classDestination;
- }
+ private boolean doCompileAndDefineSingleRound(
+ @NotNull final JavaFileManager fileManager,
+ @NotNull final List requests,
+ final int startInclusive,
+ final int endExclusive,
+ @NotNull final List toRetry) {
- private String getClassPath() {
- StringBuilder sb = new StringBuilder();
- sb.append(classDestination.getAbsolutePath());
- synchronized (additionalClassLocations) {
- for (File classLoc : additionalClassLocations) {
- sb.append(File.pathSeparatorChar).append(classLoc.getAbsolutePath());
- }
- }
- return sb.toString();
- }
+ // Create an in-memory file manager that captures compiled output
+ final InMemoryOutputFileManager outputFm = new InMemoryOutputFileManager(fileManager);
+ final StringWriter compilerOutput = new StringWriter();
- private static class CompilationState {
- int nextProbeIndex;
- boolean complete;
- String packageName;
- String fqClassName;
- }
+ final String classPathAsString = getClassPath();
+ final List compilerOptions = Arrays.asList(
+ "-cp", classPathAsString,
+ // this option allows the compiler to attempt to process all source files even if some of them fail
+ "--should-stop=ifError=GENERATE");
- private void compileHelper(
- @NotNull final List requests,
- @NotNull final List>> resolvers) {
- final MessageDigest digest;
- try {
- digest = MessageDigest.getInstance("SHA-256");
- } catch (NoSuchAlgorithmException e) {
- throw new UncheckedDeephavenException("Unable to create SHA-256 hashing digest", e);
- }
+ final MutableInt numFailures = new MutableInt(0);
+ final List globalFailures = new ArrayList<>();
+ compiler.getTask(compilerOutput,
+ outputFm,
+ diagnostic -> {
+ if (diagnostic.getKind() != Diagnostic.Kind.ERROR) {
+ return;
+ }
- final String[] basicHashText = new String[requests.size()];
- for (int ii = 0; ii < requests.size(); ++ii) {
- basicHashText[ii] = ByteUtils.byteArrToHex(digest.digest(
- requests.get(ii).classBody().getBytes(StandardCharsets.UTF_8)));
+ final JavaSourceFromString source = (JavaSourceFromString) diagnostic.getSource();
+
+ if (source == null) {
+ // If we have no source, then mark every request as a failure.
+ final UncheckedDeephavenException err = new UncheckedDeephavenException(
+ "Error Invoking Compiler, no source present in diagnostic:\n"
+ + diagnostic.getMessage(Locale.getDefault()));
+ globalFailures.add(err);
+ return;
+ }
+
+ final UncheckedDeephavenException err = new UncheckedDeephavenException("Error Compiling "
+ + source.description + "\n" + diagnostic.getMessage(Locale.getDefault()));
+ if (source.resolver.completeExceptionally(err)) {
+ // only count the first failure for each source
+ numFailures.increment();
+ }
+ },
+ compilerOptions,
+ classNamesForAnnotationProcessing,
+ requests.subList(startInclusive, endExclusive).stream()
+ .map(CompilationRequestAttempt::makeSource)
+ .collect(Collectors.toList()))
+ .call();
+
+ final String compilerOutputText = compilerOutput.toString();
+ if (!compilerOutputText.isEmpty()) {
+ log.trace().append("Compiler output:\n").append(compilerOutputText).endl();
}
- int numComplete = 0;
- final CompilationState[] states = new CompilationState[requests.size()];
- for (int ii = 0; ii < requests.size(); ++ii) {
- states[ii] = new CompilationState();
+ if (!globalFailures.isEmpty()) {
+ final RuntimeException e0 = globalFailures.get(0);
+ for (int ii = 1; ii < globalFailures.size(); ++ii) {
+ e0.addSuppressed(globalFailures.get(ii));
+ }
+ throw e0;
}
- /*
- * @formatter:off
- * 1. try to resolve without compiling; retain next hash to try
- * 2. compile all remaining with a single compilation task
- * 3. goto step 1 if any are unresolved
- * @formatter:on
- */
+ final boolean wantRetry = numFailures.get() > 0 && numFailures.get() != endExclusive - startInclusive;
- while (numComplete < requests.size()) {
- for (int ii = 0; ii < requests.size(); ++ii) {
- final CompilationState state = states[ii];
- if (state.complete) {
+ // Define compiled classes into a per-batch classloader
+ final Map compiledClasses = outputFm.getCompiledClasses();
+ log.trace().append("compilation produced ").append(compiledClasses.size())
+ .append(" classes, numFailures=").append(numFailures.get())
+ .append(", range=[").append(startInclusive).append(",").append(endExclusive).append(")")
+ .endl();
+ if (!compiledClasses.isEmpty()) {
+ // Use the current thread's context classloader as parent, unless we were explicitly provided a different
+ // one
+ final ClassLoader batchParent =
+ parentClassLoader != null ? parentClassLoader : Thread.currentThread().getContextClassLoader();;
+ final BatchClassLoader batchCl = new BatchClassLoader(batchParent, compiledClasses);
+
+ for (final CompilationRequestAttempt request : requests.subList(startInclusive, endExclusive)) {
+ if (request.resolver.getFuture().isDone()) {
+ // already failed
continue;
}
- next_probe: while (true) {
- final int pi = state.nextProbeIndex++;
- final String packageNameSuffix = "c_" + basicHashText[ii]
- + (pi == 0 ? "" : ("p" + pi))
- + "v" + JAVA_CLASS_VERSION;
-
- final QueryCompilerRequest request = requests.get(ii);
- if (pi >= MAX_CLASS_COLLISIONS) {
- Exception err = new IllegalStateException("Found too many collisions for package name root "
- + request.packageNameRoot() + ", class name=" + request.className() + ", class body "
- + "hash=" + basicHashText[ii] + " - contact Deephaven support!");
- resolvers.get(ii).completeExceptionally(err);
- state.complete = true;
- ++numComplete;
- break;
+ if (!compiledClasses.containsKey(request.fqClassName)) {
+ if (wantRetry) {
+ toRetry.add(request);
}
+ continue;
+ }
- state.packageName = request.getPackageName(packageNameSuffix);
- state.fqClassName = state.packageName + "." + request.className();
-
- for (int jj = 0; jj < ii; ++jj) {
- if (states[jj].fqClassName.equals(state.fqClassName)) {
- // collision within batch
- continue next_probe;
- }
- }
+ // Load the outer class for this request
+ final Class> clazz;
+ try {
+ clazz = batchCl.loadClass(request.fqClassName);
+ } catch (ClassNotFoundException e) {
+ request.resolver.completeExceptionally(new UncheckedDeephavenException(
+ "Failed to load compiled class: " + request.fqClassName, e));
+ continue;
+ }
- // Ask the classloader to load an existing class with this name. This might:
- // 1. Fail to find a class (returning null)
- // 2. Find a class whose body has the formula we are looking for
- // 3. Find a class whose body has a different formula (hash collision)
- Class> result = tryLoadClassByFqName(state.fqClassName, request.parameterClasses());
- if (result == null) {
- break; // we'll try to compile it
- }
-
- final boolean shouldTrace = shouldTrace(state.fqClassName);
- if (completeIfResultMatchesQueryCompilerRequest(state.packageName, request, resolvers.get(ii),
- result)) {
- if (shouldTrace) {
- log.trace().append("Successfully found existing class for ").append(state.fqClassName)
- .endl();
- }
- state.complete = true;
- ++numComplete;
- break;
- } else if (shouldTrace) {
- log.trace().append("Existing class did not match for ").append(state.fqClassName).endl();
- }
+ // Validate the identifying field
+ final String identifyingFieldValue = loadIdentifyingField(clazz);
+ if (!request.request.classBody().equals(identifyingFieldValue)) {
+ request.resolver.completeExceptionally(new IllegalStateException(
+ "Compiled class body validation failed for " + request.fqClassName));
+ continue;
}
- }
- if (numComplete == requests.size()) {
- return;
- }
+ // Notify caller with codeLog if requested
+ request.request.codeLog().ifPresent(
+ sb -> sb.append(makeFinalCode(
+ request.request.className(), request.request.classBody(), request.packageName)));
- // Couldn't resolve at least one of the requests, so try a round of compilation.
- final List compilationRequestAttempts = new ArrayList<>();
- for (int ii = 0; ii < requests.size(); ++ii) {
- final CompilationState state = states[ii];
- if (!state.complete) {
- final QueryCompilerRequest request = requests.get(ii);
- compilationRequestAttempts.add(new CompilationRequestAttempt(
- request,
- state.packageName,
- state.fqClassName,
- resolvers.get(ii)));
+ // Complete the future
+ log.trace().append("Resolving ").append(request.fqClassName).endl();
+ request.resolver.complete(clazz);
+ }
+ } else {
+ // No output at all - if there are non-failed requests, they need retry
+ for (final CompilationRequestAttempt request : requests.subList(startInclusive, endExclusive)) {
+ if (!request.resolver.getFuture().isDone() && wantRetry) {
+ toRetry.add(request);
}
}
+ }
- maybeCreateClasses(compilationRequestAttempts);
+ return wantRetry && !toRetry.isEmpty();
+ }
- // We could be running on a screwy filesystem that is slow (e.g. NFS). If we wrote a file and can't load it
- // ... then give the filesystem some time. All requests should use the same deadline.
- final long deadline = System.currentTimeMillis() + CODEGEN_TIMEOUT_MS - CODEGEN_LOOP_DELAY_MS;
- for (int ii = 0; ii < requests.size(); ++ii) {
- final CompilationState state = states[ii];
- if (state.complete) {
- continue;
- }
+ // --- Classpath construction ---
- final QueryCompilerRequest request = requests.get(ii);
- final CompletionStageFuture.Resolver> resolver = resolvers.get(ii);
- if (resolver.getFuture().isDone()) {
- state.complete = true;
- ++numComplete;
- continue;
- }
+ private String getClassPath() {
+ final StringBuilder sb = new StringBuilder(getJavaClassPath());
+ if (additionalClassPathDir != null) {
+ sb.append(File.pathSeparator).append(additionalClassPathDir.getAbsolutePath());
+ }
+ return sb.toString();
+ }
- // This request may have:
- // A. succeeded
- // B. Lost a race to another process on the same file system which is compiling the identical formula
- // C. Lost a race to another process on the same file system compiling a different formula that collides
-
- final boolean shouldTrace = shouldTrace(state.fqClassName);
- Class> clazz = tryLoadClassByFqName(state.fqClassName, request.parameterClasses());
- if (clazz == null) {
- final long waitStart = System.currentTimeMillis();
- if (shouldTrace) {
- log.trace().append("Class not loadable immediately after compile;"
- + " entering retry loop for ").append(state.fqClassName).endl();
- }
- try {
- while (clazz == null && System.currentTimeMillis() < deadline) {
- // noinspection BusyWait
- Thread.sleep(CODEGEN_LOOP_DELAY_MS);
- clazz = tryLoadClassByFqName(state.fqClassName, request.parameterClasses());
- }
- } catch (final InterruptedException ie) {
- Thread.currentThread().interrupt();
- throw new UncheckedDeephavenException("Interrupted while waiting for codegen", ie);
- }
- if (shouldTrace) {
- final long waited = System.currentTimeMillis() - waitStart;
- if (clazz != null) {
- log.trace().append("Class loaded after waiting ").append(waited)
- .append(" ms: ").append(state.fqClassName).endl();
- } else {
- log.trace().append("Retry loop timed out after ").append(waited)
- .append(" ms: ").append(state.fqClassName).endl();
- }
- }
- }
+ // --- BatchClassLoader ---
- // However, regardless of A-C, there will be *some* class being found
- if (clazz == null) {
- throw new IllegalStateException("Unable to load class after delay of " + CODEGEN_TIMEOUT_MS
- + ". state index=" + ii + ", fqClassName=" + state.fqClassName + ", parameterClasses"
- + request.parameterClasses() + ", destination=" + getClassDestination().getAbsolutePath());
- }
+ /**
+ * A classloader that defines classes from a map of name→bytes. All classes from a single compilation batch are
+ * loaded together. The parent classloader handles all other class resolution.
+ */
+ private static class BatchClassLoader extends ClassLoader {
+ private final Map classBytes;
- if (completeIfResultMatchesQueryCompilerRequest(state.packageName, request, resolver, clazz)) {
- if (shouldTrace) {
- log.trace().append("Loaded matching compiled class ").append(state.fqClassName).endl();
- }
- state.complete = true;
- ++numComplete;
- } else if (shouldTrace) {
- log.trace().append("Existing class did not match for ").append(state.fqClassName).endl();
- }
- }
+ BatchClassLoader(@NotNull ClassLoader parent, @NotNull Map classBytes) {
+ super(parent);
+ this.classBytes = classBytes;
}
- }
- private boolean completeIfResultMatchesQueryCompilerRequest(
- final String packageName,
- final QueryCompilerRequest request,
- final CompletionStageFuture.Resolver> resolver,
- final Class> result) {
- final String identifyingFieldValue = loadIdentifyingField(result);
- if (!request.classBody().equals(identifyingFieldValue)) {
- final LogEntry logOutput = log.trace().append("Hash collision for ").append(result.getName())
- .append(": loaded identifying field length=");
- if (identifyingFieldValue == null) {
- logOutput.append("null");
- } else {
- logOutput.append(identifyingFieldValue.length());
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ final byte[] bytes = classBytes.get(name);
+ if (bytes != null) {
+ return defineClass(name, bytes, 0, bytes.length);
}
- logOutput.append(", expected class body length=").append(request.classBody().length()).endl();
- return false;
+ throw new ClassNotFoundException(name);
}
+ }
- // If the caller wants a textual copy of the code we either made, or just found in the cache.
- request.codeLog()
- .ifPresent(sb -> sb.append(makeFinalCode(request.className(), request.classBody(), packageName)));
-
- // If the class we found was indeed the class we were looking for, then complete the future and return it.
- resolver.complete(result);
-
- synchronized (this) {
- // Note we are doing something kind of subtle here. We are removing an entry whose key was matched
- // by value equality and replacing it with a value-equal but reference-different string that is a
- // static member of the class we just loaded. This should be easier on the garbage collector because
- // we are replacing a calculated value with a classloaded value and so in effect we are
- // "canonicalizing" the string. This is important because these long strings stay in knownClasses
- // forever.
- knownClasses.remove(identifyingFieldValue);
- knownClasses.put(identifyingFieldValue, resolver.getFuture());
- }
+ // --- InMemoryOutputFileManager ---
- return true;
- }
+ /**
+ * A forwarding JavaFileManager that intercepts class output, storing compiled bytecode in memory rather than
+ * writing to the filesystem. All other operations are delegated.
+ */
+ private static class InMemoryOutputFileManager extends ForwardingJavaFileManager {
+ private final ConcurrentHashMap outputClasses = new ConcurrentHashMap<>();
- private Class> tryLoadClassByFqName(String fqClassName, Map> parameterClasses) {
- final boolean shouldTrace = shouldTrace(fqClassName);
- try {
- if (shouldTrace) {
- log.trace().append("Attempting to load ").append(fqClassName)
- .append(" with parameter classes ")
- .append(LogOutput.STRING_COLLECTION_FORMATTER, parameterClasses.keySet()).endl();
- }
- return getClassLoaderForFormula(parameterClasses).loadClass(fqClassName);
- } catch (ClassNotFoundException cnfe) {
- if (shouldTrace) {
- log.trace().append("Class not found for ").append(fqClassName).endl();
- }
- return null;
+ InMemoryOutputFileManager(JavaFileManager delegate) {
+ super(delegate);
}
- }
- private static String loadIdentifyingField(Class> c) {
- try {
- final Field field = c.getDeclaredField(IDENTIFYING_FIELD_NAME);
- return (String) field.get(null);
- } catch (NoSuchFieldException | IllegalAccessException e) {
- throw new IllegalStateException("Malformed class in cache", e);
+ Map getCompiledClasses() {
+ final Map result = new HashMap<>();
+ for (Map.Entry entry : outputClasses.entrySet()) {
+ result.put(entry.getKey(), entry.getValue().getBytes());
+ }
+ return result;
}
- }
- private static String makeFinalCode(String className, String classBody, String packageName) {
- if (classBody.contains("$CLASSNAME$")) {
- throw new IllegalArgumentException("QueryCompiler's support of the $CLASSNAME$ variable has been removed as"
- + " the final class name affects the compiled byte code and therefore cannot be dynamically "
- + "replaced.");
+ @Override
+ public JavaFileObject getJavaFileForOutput(
+ Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
+ if (location != StandardLocation.CLASS_OUTPUT || kind != JavaFileObject.Kind.CLASS) {
+ throw new IllegalArgumentException("In-memory output file manager only supports writing bytecode");
+ }
+ final InMemoryClassFileObject fileObject = new InMemoryClassFileObject(className);
+ InMemoryClassFileObject existing = outputClasses.put(className, fileObject);
+ if (existing != null) {
+ throw new IllegalArgumentException("Attempted to write duplicate class name " + className);
+ }
+ return fileObject;
}
- final String joinedEscapedBody = createEscapedJoinedString(classBody);
- classBody = classBody.substring(0, classBody.lastIndexOf("}"));
- classBody += " public static String " + IDENTIFYING_FIELD_NAME + " = " + joinedEscapedBody + ";\n}";
- return "package " + packageName + ";\n" + classBody;
- }
-
- /**
- * Transform a string into the corresponding Java source code that compiles into that string. This involves escaping
- * special characters, surrounding it with quotes, and (if the string is larger than the max string length for Java
- * literals), splitting it into substrings and constructing a call to String.join() that combines those substrings.
- */
- public static String createEscapedJoinedString(final String originalString) {
- return createEscapedJoinedString(originalString, DEFAULT_MAX_STRING_LITERAL_LENGTH);
+ @Override
+ public boolean hasLocation(Location location) {
+ if (location == StandardLocation.CLASS_OUTPUT) {
+ return true;
+ }
+ return super.hasLocation(location);
+ }
}
- public static String createEscapedJoinedString(final String originalString, int maxStringLength) {
- final String[] splits = splitByModifiedUtf8Encoding(originalString, maxStringLength);
+ // --- InMemoryClassFileObject ---
- // Turn each split into a Java source string by escaping it and surrounding it with "
- for (int ii = 0; ii < splits.length; ++ii) {
- final String escaped = StringEscapeUtils.escapeJava(splits[ii]);
- splits[ii] = "\"" + escaped + "\"";
+ private static class InMemoryClassFileObject extends SimpleJavaFileObject {
+ private final String className;
+ private ByteArrayOutputStream outputStream;
+ InMemoryClassFileObject(String className) {
+ super(URI.create("mem:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
+ this.className = className;
}
- assert splits.length > 0;
- if (splits.length == 1) {
- return splits[0];
+
+ @Override
+ public OutputStream openOutputStream() {
+ outputStream = new ByteArrayOutputStream();
+ return outputStream;
}
- final String formattedInnards = String.join(",\n", splits);
- return "String.join(\"\", " + formattedInnards + ")";
- }
- private static String[] splitByModifiedUtf8Encoding(final String originalString, int maxBytes) {
- final List splits = new ArrayList<>();
- // exclusive end position of the previous substring.
- int previousEnd = 0;
- // Number of bytes in the "modified UTF-8" representation of the substring we are currently scanning.
- int currentByteCount = 0;
- for (int ii = 0; ii < originalString.length(); ++ii) {
- final int bytesConsumed = calcBytesConsumed(originalString.charAt(ii));
- if (currentByteCount + bytesConsumed > maxBytes) {
- // This character won't fit in this string, so we flush the buffer.
- splits.add(originalString.substring(previousEnd, ii));
- previousEnd = ii;
- currentByteCount = 0;
+ byte[] getBytes() {
+ if (outputStream == null) {
+ throw new IllegalStateException("No bytes available for " + className);
}
- currentByteCount += bytesConsumed;
+ return outputStream.toByteArray();
}
- // At the end of the loop, either
- // 1. there are one or more characters that still need to be added to splits
- // 2. originalString was empty and so splits is empty and we need to add a single empty string to splits
- splits.add(originalString.substring(previousEnd));
- return splits.toArray(String[]::new);
}
- private static int calcBytesConsumed(final char ch) {
- if (ch == 0) {
- return 2;
- }
- if (ch <= 0x7f) {
- return 1;
- }
- if (ch <= 0x7ff) {
- return 2;
- }
- return 3;
- }
+ // --- Source representation ---
private static class JavaSourceFromString extends SimpleJavaFileObject {
final String description;
@@ -959,6 +832,7 @@ private static class JavaSourceFromString extends SimpleJavaFileObject {
this.resolver = resolver;
}
+ @Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
@@ -969,7 +843,6 @@ private static class CompilationRequestAttempt {
final String fqClassName;
final String finalCode;
final String packageName;
- final String[] splitPackageName;
final QueryCompilerRequest request;
final CompletionStageFuture.Resolver> resolver;
@@ -988,273 +861,88 @@ private CompilationRequestAttempt(
if (logEnabled) {
log.info().append("Generating code ").append(finalCode).endl();
- } else if (shouldTrace(fqClassName)) {
- log.trace().append("Generating code for ").append(fqClassName).endl();
- }
-
- splitPackageName = packageName.split("\\.");
- if (splitPackageName.length == 0) {
- final Exception err = new UncheckedDeephavenException(String.format(
- "packageName %s expected to have at least one .", packageName));
- resolver.completeExceptionally(err);
- }
- }
-
- public void ensureDirectories(@NotNull final String rootPath) {
- if (splitPackageName.length == 0) {
- // we've already failed
- return;
}
-
- final String[] truncatedSplitPackageName = Arrays.copyOf(splitPackageName, splitPackageName.length - 1);
- final Path rootPathWithPackage = Paths.get(rootPath, truncatedSplitPackageName);
- final File rpf = rootPathWithPackage.toFile();
- QueryCompilerImpl.ensureDirectories(rpf,
- () -> "Couldn't create package directories: " + rootPathWithPackage);
}
- public JavaSourceFromString makeSource() {
+ JavaSourceFromString makeSource() {
return new JavaSourceFromString(description, fqClassName, finalCode, resolver);
}
}
- private void maybeCreateClasses(
- @NotNull final List requests) {
- // Get the destination root directory (e.g. /tmp/workspace/cache/classes) and populate it with the package
- // directories (e.g. io/deephaven/test) if they are not already there. This will be useful later.
- // Also create a temp directory e.g. /tmp/workspace/cache/classes/temporaryCompilationDirectory12345
- // This temp directory will be where the compiler drops files into, e.g.
- // /tmp/workspace/cache/classes/temporaryCompilationDirectory12345/io/deephaven/test/cm12862183232603186v52_0/Formula.class
- // Foreshadowing: we will eventually atomically move cm12862183232603186v52_0 from the above to
- // /tmp/workspace/cache/classes/io/deephaven/test
- // Note: for this atomic move to work, this temp directory must be on the same file system as the destination
- // directory.
- final String rootPathAsString;
- final String tempDirAsString;
- try {
- rootPathAsString = getClassDestination().getAbsolutePath();
- for (final CompilationRequestAttempt request : requests) {
- request.ensureDirectories(rootPathAsString);
- }
-
- final Path tempPath =
- Files.createTempDirectory(Paths.get(rootPathAsString), "temporaryCompilationDirectory");
- tempDirAsString = tempPath.toFile().getAbsolutePath();
- } catch (IOException ioe) {
- Exception err = new UncheckedIOException(ioe);
- for (final CompilationRequestAttempt request : requests) {
- request.resolver.completeExceptionally(err);
- }
- return;
- }
-
- final ExecutionContext executionContext = ExecutionContext.getContext();
- final int parallelismFactor = executionContext.getOperationInitializer().parallelismFactor();
-
- final int requestsPerTask = Math.max(32, (requests.size() + parallelismFactor - 1) / parallelismFactor);
-
- final int numTasks;
- final JobScheduler jobScheduler;
-
- final boolean canParallelize = executionContext.getOperationInitializer().canParallelize();
- if (!canParallelize || parallelismFactor == 1 || requestsPerTask >= requests.size()) {
- numTasks = 1;
- jobScheduler = new ImmediateJobScheduler();
- } else {
- numTasks = (requests.size() + requestsPerTask - 1) / requestsPerTask;
- jobScheduler = new OperationInitializerJobScheduler();
- }
-
- log.trace().append("maybeCreateClasses: ").append(requests.size()).append(" requests, ")
- .append(numTasks).append(" tasks, ").append(requestsPerTask)
- .append(" requests per task, tempDir=").append(tempDirAsString).endl();
-
- final JavaFileManager fileManager = acquireFileManager();
- final AtomicReference exception = new AtomicReference<>();
- final CountDownLatch latch = new CountDownLatch(1);
- final Runnable cleanup = () -> {
- try {
- try {
- FileUtils.deleteRecursively(new File(tempDirAsString));
- } catch (Exception e) {
- // ignore errors here
- }
- try {
- releaseFileManager(fileManager);
- } catch (Exception e) {
- // ignore errors here
- }
- } finally {
- latch.countDown();
- }
- };
-
- final Consumer onError = err -> {
- if (err instanceof RuntimeException) {
- exception.set((RuntimeException) err);
- } else {
- exception.set(new UncheckedDeephavenException("Error during compilation", err));
- }
- cleanup.run();
- };
-
- jobScheduler.iterateParallel(executionContext, null, JobScheduler.DEFAULT_CONTEXT_FACTORY,
- 0, numTasks, (context, jobId, nestedErrorConsumer) -> {
- final int startInclusive = jobId * requestsPerTask;
- final int endExclusive = Math.min(requests.size(), (jobId + 1) * requestsPerTask);
- doCreateClasses(
- fileManager, requests, rootPathAsString, tempDirAsString, startInclusive, endExclusive);
- },
- () -> {
- },
- cleanup,
- onError);
+ // --- Utilities ---
+ private static String loadIdentifyingField(Class> c) {
try {
- latch.await();
- final BasePerformanceEntry perfEntry = jobScheduler.getAccumulatedPerformance();
- if (perfEntry != null) {
- QueryPerformanceRecorder.getInstance().getEnclosingNugget().accumulate(perfEntry);
- }
- final RuntimeException err = exception.get();
- if (err != null) {
- throw err;
- }
- } catch (final InterruptedException e) {
- throw new CancellationException("interrupted while compiling");
+ final Field field = c.getDeclaredField(IDENTIFYING_FIELD_NAME);
+ return (String) field.get(null);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new IllegalStateException("Malformed class in cache", e);
}
}
- private void doCreateClasses(
- @NotNull final JavaFileManager fileManager,
- @NotNull final List requests,
- @NotNull final String rootPathAsString,
- @NotNull final String tempDirAsString,
- final int startInclusive,
- final int endExclusive) {
- final List toRetry = new ArrayList<>();
- // If any of our requests fail to compile then the JavaCompiler will not write any class files at all. The
- // non-failing requests will be retried in a second pass that is expected to succeed. This enables us to
- // fulfill futures independent of each other; otherwise a single failure would taint all requests in a batch.
- final boolean wantRetry = doCreateClassesSingleRound(fileManager, requests, rootPathAsString, tempDirAsString,
- startInclusive, endExclusive, toRetry);
- if (!wantRetry) {
- return;
+ private static String makeFinalCode(String className, String classBody, String packageName) {
+ if (classBody.contains("$CLASSNAME$")) {
+ throw new IllegalArgumentException("QueryCompiler's support of the $CLASSNAME$ variable has been removed as"
+ + " the final class name affects the compiled byte code and therefore cannot be dynamically "
+ + "replaced.");
}
- final List ignored = new ArrayList<>();
- if (doCreateClassesSingleRound(fileManager, toRetry, rootPathAsString, tempDirAsString, 0, toRetry.size(),
- ignored)) {
- // We only retried compilation units that did not fail on the first pass, so we should not have any failures
- // on the second pass.
- throw new IllegalStateException("Unexpected failure during second pass of compilation");
- }
+ final String joinedEscapedBody = createEscapedJoinedString(classBody);
+ classBody = classBody.substring(0, classBody.lastIndexOf("}"));
+ classBody += " public static String " + IDENTIFYING_FIELD_NAME + " = " + joinedEscapedBody + ";\n}";
+ return "package " + packageName + ";\n" + classBody;
}
- private boolean doCreateClassesSingleRound(
- @NotNull final JavaFileManager fileManager,
- @NotNull final List requests,
- @NotNull final String rootPathAsString,
- @NotNull final String tempDirAsString,
- final int startInclusive,
- final int endExclusive,
- List toRetry) {
- final StringWriter compilerOutput = new StringWriter();
-
- final String classPathAsString = getClassPath() + File.pathSeparator + getJavaClassPath();
- final List compilerOptions = Arrays.asList(
- "-d", tempDirAsString,
- "-cp", classPathAsString,
- // this option allows the compiler to attempt to process all source files even if some of them fail
- "--should-stop=ifError=GENERATE");
-
- final MutableInt numFailures = new MutableInt(0);
- final List globalFailures = new ArrayList<>();
- compiler.getTask(compilerOutput,
- fileManager,
- diagnostic -> {
- if (diagnostic.getKind() != Diagnostic.Kind.ERROR) {
- return;
- }
-
- final JavaSourceFromString source = (JavaSourceFromString) diagnostic.getSource();
-
- if (source == null) {
- // If we have no source, then mark every request as a failure.
- final UncheckedDeephavenException err = new UncheckedDeephavenException(
- "Error Invoking Compiler, no source present in diagnostic:\n"
- + diagnostic.getMessage(Locale.getDefault()));
- globalFailures.add(err);
- return;
- }
+ /**
+ * Transform a string into the corresponding Java source code that compiles into that string.
+ */
+ public static String createEscapedJoinedString(final String originalString) {
+ return createEscapedJoinedString(originalString, DEFAULT_MAX_STRING_LITERAL_LENGTH);
+ }
- final UncheckedDeephavenException err = new UncheckedDeephavenException("Error Compiling "
- + source.description + "\n" + diagnostic.getMessage(Locale.getDefault()));
- if (source.resolver.completeExceptionally(err)) {
- // only count the first failure for each source
- numFailures.increment();
- }
- },
- compilerOptions,
- classNamesForAnnotationProcessing,
- requests.subList(startInclusive, endExclusive).stream()
- .map(CompilationRequestAttempt::makeSource)
- .collect(Collectors.toList()))
- .call();
+ public static String createEscapedJoinedString(final String originalString, int maxStringLength) {
+ final String[] splits = splitByModifiedUtf8Encoding(originalString, maxStringLength);
- final String compilerOutputText = compilerOutput.toString();
- if (!compilerOutputText.isEmpty()) {
- log.trace().append("Compiler output:\n").append(compilerOutputText).endl();
+ for (int ii = 0; ii < splits.length; ++ii) {
+ final String escaped = StringEscapeUtils.escapeJava(splits[ii]);
+ splits[ii] = "\"" + escaped + "\"";
}
-
- if (!globalFailures.isEmpty()) {
- final RuntimeException e0 = globalFailures.get(0);
- for (int ii = 1; ii < globalFailures.size(); ++ii) {
- e0.addSuppressed(globalFailures.get(ii));
- }
- throw e0;
+ assert splits.length > 0;
+ if (splits.length == 1) {
+ return splits[0];
}
+ final String formattedInnards = String.join(",\n", splits);
+ return "String.join(\"\", " + formattedInnards + ")";
+ }
- final boolean wantRetry = numFailures.get() > 0 && numFailures.get() != endExclusive - startInclusive;
-
- // The above has compiled into e.g.
- // /tmp/workspace/cache/classes/temporaryCompilationDirectory12345/io/deephaven/test/cm12862183232603186v52_0/{various
- // class files}
- // We want to atomically move it to e.g.
- // /tmp/workspace/cache/classes/io/deephaven/test/cm12862183232603186v52_0/{various class files}
- requests.subList(startInclusive, endExclusive).forEach(request -> {
- final Path srcDir = Paths.get(tempDirAsString, request.splitPackageName);
- final Path destDir = Paths.get(rootPathAsString, request.splitPackageName);
- try {
- Files.move(srcDir, destDir, StandardCopyOption.ATOMIC_MOVE);
- if (shouldTrace(request.fqClassName)) {
- log.trace().append("Successfully moved ").append(srcDir.toString()).append(" to ")
- .append(destDir.toString()).endl();
- }
- } catch (IOException ioe) {
- // The name "isDone" might be misleading here. We haven't called "complete" on the successful
- // futures yet, so the only way they would be "done" at this point is if they completed
- // exceptionally.
- final boolean hasException = request.resolver.getFuture().isDone();
-
- if (wantRetry && !Files.exists(srcDir) && !hasException) {
- // The move failed, the source directory does not exist, and this compilation unit actually
- // succeeded. However, it was not written because some other compilation unit failed. Let's schedule
- // this work to try again.
- toRetry.add(request);
- return;
- }
-
- if (!Files.exists(destDir) && !hasException) {
- // Propagate an error here only if the destination does not exist; ignoring issues related to
- // collisions with another process.
- request.resolver.completeExceptionally(new UncheckedIOException(
- "Move failed for some reason other than destination already existing", ioe));
- }
+ private static String[] splitByModifiedUtf8Encoding(final String originalString, int maxBytes) {
+ final List splits = new ArrayList<>();
+ int previousEnd = 0;
+ int currentByteCount = 0;
+ for (int ii = 0; ii < originalString.length(); ++ii) {
+ final int bytesConsumed = calcBytesConsumed(originalString.charAt(ii));
+ if (currentByteCount + bytesConsumed > maxBytes) {
+ splits.add(originalString.substring(previousEnd, ii));
+ previousEnd = ii;
+ currentByteCount = 0;
}
- });
+ currentByteCount += bytesConsumed;
+ }
+ splits.add(originalString.substring(previousEnd));
+ return splits.toArray(String[]::new);
+ }
- return wantRetry && !toRetry.isEmpty();
+ private static int calcBytesConsumed(final char ch) {
+ if (ch == 0) {
+ return 2;
+ }
+ if (ch <= 0x7f) {
+ return 1;
+ }
+ if (ch <= 0x7ff) {
+ return 2;
+ }
+ return 3;
}
/**
@@ -1331,3 +1019,5 @@ private static String getJavaClassPath() {
return javaClasspath;
}
}
+
+
diff --git a/engine/table/src/main/java/io/deephaven/engine/util/AbstractScriptSession.java b/engine/table/src/main/java/io/deephaven/engine/util/AbstractScriptSession.java
index eee2d650535..dfe085504f3 100644
--- a/engine/table/src/main/java/io/deephaven/engine/util/AbstractScriptSession.java
+++ b/engine/table/src/main/java/io/deephaven/engine/util/AbstractScriptSession.java
@@ -95,7 +95,7 @@ protected AbstractScriptSession(
this.classCacheDirectory = classCacheDirectory;
queryScope = new ScriptSessionQueryScope();
- final QueryCompiler compilerContext = QueryCompilerImpl.create(classCacheDirectory, parentClassLoader);
+ final QueryCompiler compilerContext = QueryCompilerImpl.create(classCacheDirectory);
executionContext = ExecutionContext.newBuilder()
.markSystemic()
diff --git a/engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java b/engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
index 6bb8c5d262d..477290ceff3 100644
--- a/engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
+++ b/engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
@@ -690,12 +690,6 @@ && isAnInteger(aClass.getName().substring(SCRIPT_PREFIX.length()))) {
notifiedQueryLibrary = true;
executionContext.getQueryLibrary().updateVersionString();
}
-
- try {
- QueryCompilerImpl.writeClass(classCacheDirectory, entry.getKey(), entry.getValue());
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
}
}
}
diff --git a/engine/table/src/test/java/io/deephaven/engine/table/impl/select/TestConditionFilterGeneration.java b/engine/table/src/test/java/io/deephaven/engine/table/impl/select/TestConditionFilterGeneration.java
index 9702ca6ad4a..848eb71bdb0 100644
--- a/engine/table/src/test/java/io/deephaven/engine/table/impl/select/TestConditionFilterGeneration.java
+++ b/engine/table/src/test/java/io/deephaven/engine/table/impl/select/TestConditionFilterGeneration.java
@@ -4,7 +4,6 @@
package io.deephaven.engine.table.impl.select;
import io.deephaven.engine.context.ExecutionContext;
-import io.deephaven.engine.context.QueryCompilerImpl;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.util.TableTools;
import io.deephaven.engine.table.impl.util.ModelFileGenerator;
diff --git a/engine/table/src/test/java/io/deephaven/engine/updategraph/impl/TestEventDrivenUpdateGraph.java b/engine/table/src/test/java/io/deephaven/engine/updategraph/impl/TestEventDrivenUpdateGraph.java
index 4cef60a63b6..08b0e0439f9 100644
--- a/engine/table/src/test/java/io/deephaven/engine/updategraph/impl/TestEventDrivenUpdateGraph.java
+++ b/engine/table/src/test/java/io/deephaven/engine/updategraph/impl/TestEventDrivenUpdateGraph.java
@@ -5,10 +5,9 @@
import io.deephaven.api.agg.Aggregation;
import io.deephaven.auth.AuthContext;
-import io.deephaven.configuration.DataDir;
import io.deephaven.engine.context.ExecutionContext;
-import io.deephaven.engine.context.QueryCompiler;
import io.deephaven.engine.context.QueryCompilerImpl;
+import io.deephaven.engine.context.QueryCompiler;
import io.deephaven.engine.rowset.RowSet;
import io.deephaven.engine.rowset.RowSetFactory;
import io.deephaven.engine.rowset.TrackingRowSet;
@@ -31,7 +30,6 @@
import org.apache.commons.lang3.mutable.MutableObject;
import org.junit.*;
-import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
@@ -114,10 +112,7 @@ public void run() {
}
private QueryCompiler compilerForUnitTests() {
- final Path queryCompilerDir = DataDir.get()
- .resolve("io.deephaven.engine.updategraph.impl.TestEventDrivenUpdateGraph.compilerForUnitTests");
-
- return QueryCompilerImpl.create(queryCompilerDir.toFile(), getClass().getClassLoader());
+ return QueryCompilerImpl.create();
}
@Test
diff --git a/engine/test-utils/src/main/java/io/deephaven/engine/context/TestExecutionContext.java b/engine/test-utils/src/main/java/io/deephaven/engine/context/TestExecutionContext.java
index da1d636c03b..10435309133 100644
--- a/engine/test-utils/src/main/java/io/deephaven/engine/context/TestExecutionContext.java
+++ b/engine/test-utils/src/main/java/io/deephaven/engine/context/TestExecutionContext.java
@@ -20,7 +20,7 @@ public static ExecutionContext createForUnitTests() {
.markSystemic()
.newQueryScope()
.newQueryLibrary()
- .setQueryCompiler(QueryCompilerImpl.createForUnitTests())
+ .setQueryCompiler(QueryCompilerImpl.create())
.setUpdateGraph(UPDATE_GRAPH)
.setOperationInitializer(OPERATION_INITIALIZATION)
.build();
diff --git a/extensions/parquet/benchmark/src/benchmark/java/io/deephaven/benchmark/parquet/table/TableWriteBenchmark.java b/extensions/parquet/benchmark/src/benchmark/java/io/deephaven/benchmark/parquet/table/TableWriteBenchmark.java
index 9ddfb85baf8..dd10bb8c06e 100644
--- a/extensions/parquet/benchmark/src/benchmark/java/io/deephaven/benchmark/parquet/table/TableWriteBenchmark.java
+++ b/extensions/parquet/benchmark/src/benchmark/java/io/deephaven/benchmark/parquet/table/TableWriteBenchmark.java
@@ -51,7 +51,7 @@ public void setupEnv() throws IOException {
.newQueryLibrary()
.newQueryScope()
.setQueryCompiler(
- QueryCompilerImpl.create(rootPath.resolve("cache").toFile(), getClass().getClassLoader()))
+ QueryCompilerImpl.create())
.build();
exContextCloseable = context.open();
diff --git a/extensions/performance/src/test/java/io/deephaven/engine/table/impl/util/TestUpdateAncestorViz.java b/extensions/performance/src/test/java/io/deephaven/engine/table/impl/util/TestUpdateAncestorViz.java
index dceb067d235..c3ceb18b917 100644
--- a/extensions/performance/src/test/java/io/deephaven/engine/table/impl/util/TestUpdateAncestorViz.java
+++ b/extensions/performance/src/test/java/io/deephaven/engine/table/impl/util/TestUpdateAncestorViz.java
@@ -56,8 +56,7 @@ public void before() throws IOException {
cacheDir.deleteOnExit();
executionContext = ExecutionContext.newBuilder().newQueryLibrary().newQueryScope()
- .setQueryCompiler(QueryCompilerImpl.create(
- cacheDir, TestUpdateAncestorViz.class.getClassLoader()))
+ .setQueryCompiler(QueryCompilerImpl.create())
.setOperationInitializer(ForkJoinPoolOperationInitializer.fromCommonPool())
.setUpdateGraph(defaultUpdateGraph).build().withAuthContext(new AuthContext.Anonymous());
}