Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable SPI functionality in GraalVM native mode #20

Merged
merged 9 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ To make testing simple, the extension provides the `HazelcastServerTestResource`
## Limitations (native mode)
- Default Java serialization is not supported
- User code deployment is not supported
- Hazelcast SPI support can be limited on OSGi
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
package io.quarkus.hazelcast.client.deployment;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Set;

import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;

import com.hazelcast.client.cache.impl.HazelcastClientCachingProvider;
import com.hazelcast.client.impl.ClientExtension;
import com.hazelcast.client.impl.spi.ClientProxyFactory;
Expand All @@ -29,30 +22,30 @@
import com.hazelcast.nio.serialization.Serializer;
import com.hazelcast.nio.ssl.BasicSSLContextFactory;
import com.hazelcast.partition.MigrationListener;
import com.hazelcast.spi.discovery.DiscoveryStrategy;
import com.hazelcast.spi.discovery.DiscoveryStrategyFactory;
import com.hazelcast.spi.discovery.NodeFilter;
import com.hazelcast.spi.discovery.multicast.MulticastDiscoveryStrategy;
import com.hazelcast.topic.MessageListener;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.util.ServiceUtil;
import io.quarkus.hazelcast.client.runtime.HazelcastClientBytecodeRecorder;
import io.quarkus.hazelcast.client.runtime.HazelcastClientConfig;
import io.quarkus.hazelcast.client.runtime.HazelcastClientProducer;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;

import java.io.IOException;

class HazelcastClientProcessor {

Expand All @@ -67,10 +60,12 @@ void enableSSL(BuildProducer<ExtensionSslNativeSupportBuildItem> ssl) {
}

@BuildStep
void registerServiceProviders(BuildProducer<ServiceProviderBuildItem> services) throws IOException {
registerServiceProviders(DiscoveryStrategyFactory.class, services);
registerServiceProviders(ClientExtension.class, services);
registerServiceProviders(JsonFactory.class, services);
void registerServiceProviders(BuildProducer<GeneratedResourceBuildItem> generatedResources,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
BuildProducer<NativeImageResourceBuildItem> resources) throws IOException {
registerServiceProviders(DiscoveryStrategyFactory.class, resources, reflectiveClasses, generatedResources);
registerServiceProviders(ClientExtension.class, resources, reflectiveClasses, generatedResources);
registerServiceProviders(JsonFactory.class, resources, reflectiveClasses, generatedResources);
}

@BuildStep
Expand Down Expand Up @@ -137,34 +132,14 @@ void registerCustomConfigReplacerClasses(BuildProducer<ReflectiveClassBuildItem>
PropertyReplacer.class));
}

@BuildStep
void registerCustomDiscoveryStrategiesClasses(BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
BuildProducer<ReflectiveHierarchyBuildItem> reflectiveClassHierarchies,
BuildProducer<ReflectiveHierarchyIgnoreWarningBuildItem> ignoreWarnings) {

registerTypeHierarchy(reflectiveClassHierarchies, ignoreWarnings,
DiscoveryStrategy.class,
NodeFilter.class);

reflectiveClasses.produce(new ReflectiveClassBuildItem(false, false, MulticastDiscoveryStrategy.class));
reflectiveClasses.produce(new ReflectiveClassBuildItem(false, false,
"com.hazelcast.aws.AwsDiscoveryStrategy",
"com.hazelcast.aws.AwsDiscoveryStrategyFactory",
"com.hazelcast.gcp.GcpDiscoveryStrategy",
"com.hazelcast.gcp.GcpDiscoveryStrategyFactory"));

reflectiveClasses.produce(new ReflectiveClassBuildItem(false, false,
"com.hazelcast.kubernetes.HazelcastKubernetesDiscoveryStrategyFactory",
"com.hazelcast.kubernetes.HazelcastKubernetesDiscoveryStrategy"));
}

void registerServiceProviders(Class<?> klass, BuildProducer<ServiceProviderBuildItem> services) throws IOException {
void registerServiceProviders(Class<?> klass, BuildProducer<NativeImageResourceBuildItem> resources, BuildProducer<ReflectiveClassBuildItem> reflectiveClasses, BuildProducer<GeneratedResourceBuildItem> generatedResources) throws IOException {
String service = "META-INF/services/" + klass.getName();

Set<String> implementations = ServiceUtil
.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), service);
for (String impl : ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), service)) {
reflectiveClasses.produce(new ReflectiveClassBuildItem(false, false, impl));
}

services.produce(new ServiceProviderBuildItem(klass.getName(), new ArrayList<>(implementations)));
resources.produce(new NativeImageResourceBuildItem(service));
}

@BuildStep
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@


<properties>
<quarkus.version>1.3.0.Final</quarkus.version>
<hazelcast.version>4.0</hazelcast.version>
<quarkus.version>1.3.2.Final</quarkus.version>
<hazelcast.version>4.0.1</hazelcast.version>

<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.quarkus.hazelcast.client.runtime.graal;

import com.hazelcast.logging.ILogger;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import static com.hazelcast.internal.nio.IOUtil.closeResource;

final class ServiceLoaderUtils {
private ServiceLoaderUtils() {
}

/*
Expanded version of {@link com.hazelcast.internal.util.ServiceLoader#parse(ServiceLoader.URLDefinition)
that's additionally parameterized with {@link ILogger} }
*/
static Set<Target_ServiceDefinition> parse(URL url, ClassLoader classLoader, ILogger logger) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Could you add javadoc explaining what this method does? Or maybe make this method name more explanatory?
  2. Wouldn't it be worth to add some unit tests for his class and other you added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add docs around the Graal substitution and explain the problem there as well.

I'm working on tests at the moment. Unit testing is useless, we need an integration test that loads a custom extension via SPI

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is covered by Hasan's test suite

try {
Set<Target_ServiceDefinition> names = new HashSet<>();
BufferedReader r = null;
try {
r = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8));
while (true) {
String line = r.readLine();
if (line == null) {
break;
}
int comment = line.indexOf('#');
if (comment >= 0) {
line = line.substring(0, comment);
}
String name = line.trim();
if (name.length() == 0) {
continue;
}
names.add(new Target_ServiceDefinition(name, classLoader));
}
} finally {
closeResource(r);
}
return names;
} catch (Exception e) {
logger.severe(e);
}
return Collections.emptySet();
}

static ClassLoader resolveClassloader(ClassLoader classLoader) {
return classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

/*
* JCache is not supported under GraalVM, therefore JCache detection infrastructure can be removed.
*/
@TargetClass(JCacheDetector.class)
public final class Target_JCacheDetector {

Expand All @@ -13,6 +16,7 @@ public static boolean isJCacheAvailable(ClassLoader classLoader) {
return false;
}


@Substitute
public static boolean isJCacheAvailable(ClassLoader classLoader, ILogger logger) {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkus.hazelcast.client.runtime.graal;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

import static com.hazelcast.internal.util.Preconditions.isNotNull;

@Substitute
@TargetClass(className = "com.hazelcast.internal.util.ServiceLoader", innerClass = "ServiceDefinition")
final class Target_ServiceDefinition {
private final String className;
private final ClassLoader classLoader;

public Target_ServiceDefinition(String className, ClassLoader classLoader) {
this.className = isNotNull(className, "className");
this.classLoader = isNotNull(classLoader, "classLoader");
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

Target_ServiceDefinition that = (Target_ServiceDefinition) o;
if (!classLoader.equals(that.classLoader)) {
return false;
}
return className.equals(that.className);
}

@Override
public int hashCode() {
int result = className.hashCode();
result = 31 * result + classLoader.hashCode();
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.quarkus.hazelcast.client.runtime.graal;

import com.hazelcast.internal.util.ServiceLoader;
import com.hazelcast.logging.ILogger;
import com.hazelcast.logging.Logger;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

import static io.quarkus.hazelcast.client.runtime.graal.ServiceLoaderUtils.parse;
import static io.quarkus.hazelcast.client.runtime.graal.ServiceLoaderUtils.resolveClassloader;

@TargetClass(ServiceLoader.class)
public final class Target_ServiceLoader {

@Alias
private static ILogger LOGGER;

/*
Hazelcast SPI relies on the classpath presence of multiple _META-INF/services/com.foo.Bar_ files (for example, _com.hazelcast.spi.discovery.multicast.DiscoveryStrategyFactory_).

However, the internal implementation of ServiceLoader doesn't play along with Substrate since
it doesn't really feature a _normal_ classpath nor classloading but a mere _simulation_ of these.

URLs to classpath resources located in different jars normally look like:

jar:file:/work/application/lib/com.hazelcast.hazelcast-4.0.1.jar!/META-INF/services/com.hazelcast.spi.discovery.DiscoveryStrategyFactory
jar:file:/work/application/lib/com.hazelcast.hazelcast-kubernetes-2.0.1.jar!/META-INF/services/com.hazelcast.spi.discovery.DiscoveryStrategyFactory

but under Substrate, they become _resources_ and share the same URL (but `URL#openStream` leads to different content) which requires special handling:

resource:META-INF/services/com.hazelcast.spi.discovery.DiscoveryStrategyFactory
resource:META-INF/services/com.hazelcast.spi.discovery.DiscoveryStrategyFactory

In order to make it work under GraalVM, we need to give up extra URL processing and don't enforce uniqueness for URLs, but for their content instead.
We need to make sure that we're properly handling system classloader represented as (null)
*/
@Substitute
private static Set<Target_ServiceDefinition> getServiceDefinitions(String factoryId, ClassLoader classLoader) {
ClassLoader actual = resolveClassloader(classLoader);

Set<Target_ServiceDefinition> services = new HashSet<>();

try {
Enumeration<URL> resources = actual
.getResources("META-INF/services/" + factoryId);

while (resources.hasMoreElements()) {
services.addAll(parse(resources.nextElement(), actual, LOGGER));
}
} catch (IOException e) {
LOGGER.severe(e);
}

if (services.isEmpty()) {
Logger.getLogger(ServiceLoader.class).finest(
"Service loader could not load 'META-INF/services/" + factoryId + "'. It may be empty or does not exist.");
}
return services;
}
}