org.neo4j.gds.compat.ProxyUtil Maven / Gradle / Ivy
Show all versions of neo4j-kernel-adapter-api Show documentation
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.neo4j.gds.compat;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import org.neo4j.gds.annotation.GenerateBuilder;
import org.neo4j.gds.annotation.SuppressForbidden;
import org.neo4j.logging.Log;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
public final class ProxyUtil {
private static final AtomicBoolean LOG_ENVIRONMENT = new AtomicBoolean(true);
private static final AtomicReference LOG_MESSAGES = new AtomicReference<>(new BufferingLog());
private static final Map, ProxyInfo> PROXY_INFO_CACHE = new ConcurrentHashMap<>();
public static > PROXY findProxy(
Class factoryClass,
MayLogToStdout mayLogToStdout
) {
return findProxyInfo(factoryClass)
.proxy()
.apply(mayLogToStdout);
}
public static > ProxyInfo findProxyInfo(Class factoryClass) {
// we know that this type is correct due to the signature of loadAndValidateProxyInfo
// we lose the type information because of the map API
//noinspection unchecked
return (ProxyInfo) PROXY_INFO_CACHE.computeIfAbsent(
factoryClass,
fc -> loadAndValidateProxyInfo(factoryClass)
);
}
public static void dumpLogMessages(Log log) {
var buffer = LOG_MESSAGES.getAndSet(NullLog.INSTANCE);
buffer.replayInto(log);
}
private static > ProxyInfo loadAndValidateProxyInfo(Class factoryClass) {
var log = LOG_MESSAGES.get();
var availabilityLog = new StringJoiner(", ", "GDS compatibility: ", "");
availabilityLog.setEmptyValue("");
var proxyInfo = loadProxyInfo(factoryClass);
// log any errors while looking for the GDS version, but continue since we have a fallback value
proxyInfo.gdsVersion().error().ifPresent(e -> e.log(log));
try {
// log any errors while looking for the Neo4j version
// stop execution since we don't have a valid Neo4j version fallback value
var neo4jVersionError = proxyInfo.neo4jVersion().error();
if (neo4jVersionError.isPresent()) {
neo4jVersionError.get().log(log);
throw new RuntimeException(neo4jVersionError.get().reason());
}
// log any errors while trying to find all proxies
// stop execution since we don't have any proxy instances
var proxyError = proxyInfo.error();
if (proxyError.isPresent()) {
proxyError.get().log(log);
throw new RuntimeException(proxyError.get().reason());
}
// log availability of all proxy implementations
proxyInfo.availability().forEach((name, availability) -> {
availabilityLog.add(String.format(
Locale.ENGLISH,
"for %s -- %s",
name,
availability ? "available" : "not available"
));
});
// make sure that we have a proxy available
var factory = proxyInfo.factory();
if (factory.isEmpty()) {
return proxyInfo;
}
availabilityLog.add("selected: " + factory.get().description());
var builder = ProxyInfoBuilder.builder(proxyInfo);
var proxy = factory.get().load();
builder.maybeProxy(__ -> proxy);
return builder.build();
} finally {
if (LOG_ENVIRONMENT.getAndSet(false)) {
log.log(
LogLevel.DEBUG,
"Java vendor: [%s] Java version: [%s] Java home: [%s] GDS version: [%s] Detected Neo4j version: [%s]",
proxyInfo.javaInfo().javaVendor(),
proxyInfo.javaInfo().javaVersion(),
proxyInfo.javaInfo().javaHome(),
proxyInfo.gdsVersion().gdsVersion(),
proxyInfo.neo4jVersion().neo4jVersion()
);
}
var availability = availabilityLog.toString();
if (!availability.isEmpty()) {
log.log(LogLevel.INFO, availability);
}
}
}
private static > ProxyInfo loadProxyInfo(Class factoryClass) {
var builder = ProxyInfoBuilder
.builder()
.availability(new LinkedHashMap<>())
.factoryType(factoryClass)
.neo4jVersion(NEO4J_VERSION_INFO)
.gdsVersion(GdsVersionInfoProvider.GDS_VERSION_INFO)
.javaInfo(JAVA_INFO);
try {
var availableProxies = ServiceLoader
.load(factoryClass, factoryClass.getClassLoader())
.stream()
.map(ServiceLoader.Provider::get)
.filter(f -> {
var canLoad = f.canLoad(NEO4J_VERSION_INFO.neo4jVersion());
builder.availability().put(f.description(), canLoad);
return canLoad;
})
.toList();
builder.factory(availableProxies.stream().findFirst());
} catch (Exception e) {
builder.error(new ErrorInfo("Could not load GDS proxy: " + e.getMessage(), LogLevel.ERROR, e));
}
return builder.build();
}
private static final Neo4jVersionInfo NEO4J_VERSION_INFO = loadNeo4jVersion();
private static Neo4jVersionInfo loadNeo4jVersion() {
try {
var neo4jVersion = Neo4jVersion.findNeo4jVersion();
var error =
neo4jVersion.isSupported()
? Optional.empty()
: Optional.of(new ErrorInfo(
"GDS does not support Neo4j version " + neo4jVersion,
LogLevel.ERROR,
new UnsupportedOperationException("GDS does not support Neo4j version " + neo4jVersion)
));
return new Neo4jVersionInfo(neo4jVersion, error);
} catch (Exception e) {
var error = new ErrorInfo("Could not determine Neo4j version: " + e.getMessage(), LogLevel.ERROR, e);
return new Neo4jVersionInfo(new Neo4jVersion.Unsupported(-1, -1, "unknown"), Optional.of(error));
}
}
private static final JavaInfo JAVA_INFO = loadJavaInfo();
private static JavaInfo loadJavaInfo() {
return new JavaInfo(
System.getProperty("java.vendor"),
System.getProperty("java.version"),
System.getProperty("java.home")
);
}
/**
* We want to test this class as if we just started the JVM,
* i.e. the state of whether we have already logged is not shared.
*
* This is not tied to the lifecycle of the test instance but to the
* lifecycle of the JVM.
*
* We allow tests to reset the ProxyUtil with a test-only method to
* avoid having to fork a new JVM fer every test.
*/
@TestOnly
static void resetState() {
LOG_ENVIRONMENT.set(true);
LOG_MESSAGES.set(new BufferingLog());
PROXY_INFO_CACHE.clear();
}
public enum MayLogToStdout {
YES, NO
}
@GenerateBuilder
public record ProxyInfo(
@NotNull Class factoryType,
@NotNull Neo4jVersionInfo neo4jVersion,
@NotNull GdsVersionInfoProvider.GdsVersionInfo gdsVersion,
@NotNull JavaInfo javaInfo,
@NotNull LinkedHashMap availability,
@NotNull Optional factory,
@NotNull Optional error,
@NotNull Optional> maybeProxy
) {
Function proxy() {
return this.maybeProxy.orElse(this::unsupported);
}
@SuppressForbidden(reason = "We need to log to stdout here")
private U unsupported(MayLogToStdout mayLogToStdout) {
if (mayLogToStdout == MayLogToStdout.YES) {
// since we are throwing and potentially aborting the database startup, we might as well
// log all messages we have accumulated so far to provide more debugging context
ProxyUtil.dumpLogMessages(OutputStreamLog.builder(System.out).build().log());
}
throw new LinkageError(String.format(
Locale.ENGLISH,
"GDS %s is not compatible with Neo4j version: %s",
gdsVersion.gdsVersion(),
neo4jVersion.neo4jVersion().fullVersion()
));
}
}
public record Neo4jVersionInfo(
@NotNull Neo4jVersion neo4jVersion,
@NotNull Optional error
) {
}
public record ErrorInfo(
@NotNull String message,
@NotNull LogLevel logLevel,
@NotNull Throwable reason
) {
void log(ProxyLog log) {
log.log(logLevel(), message(), reason());
}
}
public enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR
}
public record JavaInfo(
@NotNull String javaVendor,
@NotNull String javaVersion,
@NotNull String javaHome
) {
}
interface ProxyLog {
void log(LogLevel logLevel, String message, Throwable reason);
void log(LogLevel logLevel, String format, Object... args);
void replayInto(Log log);
}
private record LogMessage(
@NotNull LogLevel logLevel,
@NotNull String message,
Optional reason,
Optional