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

Upgrade to JC Tools 3.0 changes queue behavior when compiled natively on GraalVM. #10376

Open
michael-simons opened this issue Jun 25, 2020 · 12 comments

Comments

@michael-simons
Copy link

The upgrade to JC Tools 3.0 from 4.1.45 to 4.1.46 changes queue behavior when compiled natively on GraalVM.

For Netty 4.1.45 we used the following substitution in the Neo4j Java Driver

@TargetClass(className = "io.netty.channel.nio.NioEventLoop")
 final class Target_io_netty_channel_nio_NioEventLoop {

     @Substitute
     private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
         return new LinkedBlockingDeque<>();
     }
 }

to force Netty to not use JC Tools Mscp queues. This is the same substitution that Quarkus uses (see https://github.com/quarkusio/quarkus/blob/0513ad94fd93f4ac377b796521a03dd3a52a156b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java#L313-L320) and which I proposed to include in Netty itself here (#9989).

This substitution is not enough anymore since Netty 4.1.46.

Netty 4.1.46 updated JC Tools to 3.0.
When I downgrade this to 2.1.1 again, the substitution works as expected.

I have the suspicion that this change here JCTools/JCTools@2ac1a66 triggers a different behavior in native image mode.

This can be mitigated by allowing unsafe, reflective field access to a couple of classes and fields of JC Tools in a reflection-config.json.

 {
    "name" : "io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField",
    "fields": [
      {"name": "producerIndex", "allowUnsafeAccess": true}
    ]
  },
  {
    "name" : "io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField",
    "fields": [
      {"name": "producerLimit", "allowUnsafeAccess": true}
    ]
  },
  {
    "name" : "io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField",
    "fields": [
      {"name": "consumerIndex", "allowUnsafeAccess": true}
    ]
  },
  {
    "name" : "io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields",
    "fields": [
      {"name": "producerIndex", "allowUnsafeAccess": true}
    ]
  },
  {
    "name" : "io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields",
    "fields": [
      {"name": "producerLimit", "allowUnsafeAccess": true}
    ]
  },
  {
    "name" : "io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields",
    "fields": [
      {"name": "consumerIndex", "allowUnsafeAccess": true}
    ]
  }

If this is not done, any thing bootstrapping Netty will fail with something in this area:

Exception in thread "main" java.lang.ExceptionInInitializerError
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:290)
	at java.lang.Class.ensureInitialized(DynamicHub.java:499)
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:235)
	at java.lang.Class.ensureInitialized(DynamicHub.java:499)
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:235)
	at java.lang.Class.ensureInitialized(DynamicHub.java:499)
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:235)
	at java.lang.Class.ensureInitialized(DynamicHub.java:499)
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:235)
	at java.lang.Class.ensureInitialized(DynamicHub.java:499)
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:235)
	at java.lang.Class.ensureInitialized(DynamicHub.java:499)
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:235)
	at java.lang.Class.ensureInitialized(DynamicHub.java:499)
	at io.netty.util.internal.PlatformDependent$Mpsc.newMpscQueue(PlatformDependent.java:934)
	at io.netty.util.internal.PlatformDependent.newMpscQueue(PlatformDependent.java:945)
	at io.netty.channel.nio.NioEventLoop.newTaskQueue0(NioEventLoop.java:279)
	at io.netty.channel.nio.NioEventLoop.newTaskQueue(NioEventLoop.java:150)
	at io.netty.channel.nio.NioEventLoop.<init>(NioEventLoop.java:138)
	at io.netty.channel.nio.NioEventLoopGroup.newChild(NioEventLoopGroup.java:146)
	at io.netty.channel.nio.NioEventLoopGroup.newChild(NioEventLoopGroup.java:37)
	at io.netty.util.concurrent.MultithreadEventExecutorGroup.<init>(MultithreadEventExecutorGroup.java:84)
	at io.netty.util.concurrent.MultithreadEventExecutorGroup.<init>(MultithreadEventExecutorGroup.java:58)
	at io.netty.channel.MultithreadEventLoopGroup.<init>(MultithreadEventLoopGroup.java:52)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:96)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:91)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:72)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:52)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:44)
	at ac.simons.netty.example.Client.main(Client.java:21)
Caused by: java.lang.RuntimeException: java.lang.NoSuchFieldException: producerIndex
	at io.netty.util.internal.shaded.org.jctools.util.UnsafeAccess.fieldOffset(UnsafeAccess.java:111)
	at io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields.<clinit>(BaseMpscLinkedArrayQueue.java:41)
	at com.oracle.svm.core.hub.ClassInitializationInfo.invokeClassInitializer(ClassInitializationInfo.java:350)
	at com.oracle.svm.core.hub.ClassInitializationInfo.initialize(ClassInitializationInfo.java:270)
	... 29 more
Caused by: java.lang.NoSuchFieldException: producerIndex
	at java.lang.Class.getDeclaredField(DynamicHub.java:2411)
	at io.netty.util.internal.shaded.org.jctools.util.UnsafeAccess.fieldOffset(UnsafeAccess.java:107)
	... 32 more

I have created the most simple reproducer I could: https://github.com/michael-simons/native-netty-timeserver-and-client Which is basically the vanilla server/client code from the Time server example of yours.

It can be compiled and run natively on the GraalVM. If you want to see the error, take this commit and update to anything >= 4.1.46 and stuff will fail.

The next commit contains the fixes.

Expected behavior or outcome

Nothing concrete at the moment: We at Neo4j can work around this, but it might have an impact on other projects that use Netty but are aiming for native compilation.

The substation mechanism seems brittle to say at least. Under the light of this experience here, I'd rather think about closing my pr but suggest picking up the suggested reflection config above for a future version of Netty. I personally don't feel to well to refer to shaded things (which we shade again in our driver).

As this was discovered by the kind Quarkus team here quarkusio/quarkus#10200, I'm also paging
@gsmet and @cescoffier as they might run into the same issues with the Netty substitutions in Quarkus itself.

Maybe @nitsanw has an idea why the movement of the the static field initializations are now triggered early?

@franz1981
Copy link
Contributor

franz1981 commented Jun 25, 2020

Hi, just a qq: why not use https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueue.java (or the unbounded version if needed) that is just using j.c.u. plain AtomicXXX classes that should be already accounted in the substitutions (hopefully)?

@cescoffier
Copy link

Thanks @michael-simons for the ping! Yes, we will have to update the native configuration for Netty when we update.

@cescoffier
Copy link

... we already using 4.1.49, so it needs to be updated.

@michael-simons
Copy link
Author

michael-simons commented Jun 25, 2020

Hi @franz1981. Thanks for chiming in.

I was there as well in my research and found this piece of code in Netty:

private static final class Mpsc {
private static final boolean USE_MPSC_CHUNKED_ARRAY_QUEUE;
private Mpsc() {
}
static {
Object unsafe = null;
if (hasUnsafe()) {
// jctools goes through its own process of initializing unsafe; of
// course, this requires permissions which might not be granted to calling code, so we
// must mark this block as privileged too
unsafe = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
// force JCTools to initialize unsafe
return UnsafeAccess.UNSAFE;
}
});
}
if (unsafe == null) {
logger.debug("org.jctools-core.MpscChunkedArrayQueue: unavailable");
USE_MPSC_CHUNKED_ARRAY_QUEUE = false;
} else {
logger.debug("org.jctools-core.MpscChunkedArrayQueue: available");
USE_MPSC_CHUNKED_ARRAY_QUEUE = true;
}
}
static <T> Queue<T> newMpscQueue(final int maxCapacity) {
// Calculate the max capacity which can not be bigger then MAX_ALLOWED_MPSC_CAPACITY.
// This is forced by the MpscChunkedArrayQueue implementation as will try to round it
// up to the next power of two and so will overflow otherwise.
final int capacity = max(min(maxCapacity, MAX_ALLOWED_MPSC_CAPACITY), MIN_MAX_MPSC_CAPACITY);
return USE_MPSC_CHUNKED_ARRAY_QUEUE ? new MpscChunkedArrayQueue<T>(MPSC_CHUNK_SIZE, capacity)
: new MpscGrowableAtomicArrayQueue<T>(MPSC_CHUNK_SIZE, capacity);
}
static <T> Queue<T> newMpscQueue() {
return USE_MPSC_CHUNKED_ARRAY_QUEUE ? new MpscUnboundedArrayQueue<T>(MPSC_CHUNK_SIZE)
: new MpscUnboundedAtomicArrayQueue<T>(MPSC_CHUNK_SIZE);
}
}

I think that should actually already do this. We also set io.netty.noUnsafe=true in our image config, but for whatever reason, it doesn't seem to have an effect. That's why I brought up the hint to the static initializer blocks vs. method calls.

To be clear: I have no interested in using anything from JCTools directly at all :) We are not calling those anywhere.

@michael-simons
Copy link
Author

... we already using 4.1.49, so it needs to be updated.

I think so, yes!

cescoffier added a commit to cescoffier/quarkus that referenced this issue Jun 25, 2020
…vely and the unsafe configuration must be adjusted.

Follow up of the discussion from netty/netty#10376
@franz1981
Copy link
Contributor

franz1981 commented Jun 25, 2020

@michael-simons
I'm surprised that io.netty.noUnsafe isn't working as expected TBH: what debug logs says re
16:35:43.491 [main] DEBUG io.netty.util.internal.PlatformDependent - org.jctools-core.MpscChunkedArrayQueue: unavailable?

let me know if forcing with this

 @TargetClass(className = "io.netty.channel.nio.NioEventLoop")
 final class Target_io_netty_channel_nio_NioEventLoop {

     @Substitute
     private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
         if (maxPendingTasks == Integer.MAX_VALUE) 
              return new MpscUnboundedAtomicArrayQueue<T>(1024);
         final int capacity = Math.max(Math.min(maxPendingTasks, 1073741824), 2048);
         return new MpscChunkedAtomicArrayQueue<>(1024, capacity);
     }
 }

help.

I see too that the original code

        static <T> Queue<T> newMpscQueue(final int maxCapacity) {
            // Calculate the max capacity which can not be bigger then MAX_ALLOWED_MPSC_CAPACITY.
            // This is forced by the MpscChunkedArrayQueue implementation as will try to round it
            // up to the next power of two and so will overflow otherwise.
            final int capacity = Math.max(Math.min(maxCapacity, 1073741824), 2048);
            return USE_MPSC_CHUNKED_ARRAY_QUEUE ? new MpscChunkedArrayQueue<T>(MPSC_CHUNK_SIZE, capacity)
                                                : new MpscGrowableAtomicArrayQueue<T>(MPSC_CHUNK_SIZE, capacity);
        }

isn't "right": MpscGrowableAtomicArrayQueue and MpscChunkedArrayQueue behave differently....seems a bug to me, but maybe @Scottmitch knows the answer :)

@franz1981
Copy link
Contributor

@michael-simons OOps, I've missed the

To be clear: I have no interested in using anything from JCTools directly at all :) We are not calling those anywhere.

@michael-simons
Copy link
Author

I hope that didn’t came across ignorant. Should not be. I use it only through Netty. But I’ll see that I create a reproducer for pure JCtools.

@michael-simons
Copy link
Author

It seems that something just doesn't care about me setting -Dio.netty.noUnsafe=true in the native-image.properties

After fiddling around to get JDK logging out of Graal, I see:

[2020-06-26 08:47:04 074] [FINE   ] Platform: MacOS 
[2020-06-26 08:47:04 079] [FINE   ] -Dio.netty.noUnsafe: false 
[2020-06-26 08:47:04 079] [FINE   ] Java version: 11 
[2020-06-26 08:47:04 079] [FINE   ] sun.misc.Unsafe.theUnsafe: available 
[2020-06-26 08:47:04 079] [FINE   ] sun.misc.Unsafe.copyMemory: available 
[2020-06-26 08:47:04 079] [FINE   ] java.nio.Buffer.address: available 
[2020-06-26 08:47:04 079] [FINE   ] direct buffer constructor: unavailable 
[2020-06-26 08:47:04 080] [FINE   ] java.nio.Bits.unaligned: unavailable true 
[2020-06-26 08:47:04 080] [FINE   ] jdk.internal.misc.Unsafe.allocateUninitializedArray(int): unavailable 
[2020-06-26 08:47:04 080] [FINE   ] java.nio.DirectByteBuffer.<init>(long, int): unavailable 
[2020-06-26 08:47:04 080] [FINE   ] sun.misc.Unsafe: available 
[2020-06-26 08:47:04 080] [FINE   ] maxDirectMemory: 27487790640 bytes (maybe) 
[2020-06-26 08:47:04 080] [FINE   ] -Dio.netty.tmpdir: /var/folders/4r/xj187z157v9dtp2t46sh284c0000gn/T (java.io.tmpdir) 
[2020-06-26 08:47:04 080] [FINE   ] -Dio.netty.bitMode: 64 (sun.arch.data.model) 
[2020-06-26 08:47:04 080] [FINE   ] -Dio.netty.maxDirectMemory: -1 bytes 
[2020-06-26 08:47:04 080] [FINE   ] -Dio.netty.uninitializedArrayAllocationThreshold: -1 
[2020-06-26 08:47:04 080] [FINE   ] java.nio.ByteBuffer.cleaner(): available 
[2020-06-26 08:47:04 080] [FINE   ] -Dio.netty.noPreferDirect: false 

gsmet pushed a commit to gsmet/quarkus that referenced this issue Jun 30, 2020
…vely and the unsafe configuration must be adjusted.

Follow up of the discussion from netty/netty#10376
@nitsanw
Copy link
Contributor

nitsanw commented Jul 6, 2020

@michael-simons @franz1981 WRT
"I'm surprised that io.netty.noUnsafe isn't working as expected"

If the program is AOT compiled the flag should be set during the build, the runtime value will be ignored IIUC. Is that the case here?

@rmannibucau
Copy link
Contributor

Same here, I need to either set noUnsafe on the binary (which is highly unexpected and was not the case before since netty also test if running in a native image) or way more finely tune the reflection:

          <reflections>
            <reflection>
              <name>io.netty.buffer.AbstractByteBufAllocator</name>
              <allDeclaredMethods>true</allDeclaredMethods>
            </reflection>
            <reflection>
              <name>io.netty.util.ReferenceCountUtil</name>
              <allDeclaredMethods>true</allDeclaredMethods>
            </reflection>
            <reflection>
              <name>io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField</name>
              <fields>
                <field>
                  <name>producerIndex</name>
                  <allowUnsafeAccess>true</allowUnsafeAccess>
                </field>
              </fields>
            </reflection>
            <reflection>
              <name>io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField</name>
              <fields>
                <field>
                  <name>producerLimit</name>
                  <allowUnsafeAccess>true</allowUnsafeAccess>
                </field>
              </fields>
            </reflection>
            <reflection>
              <name>io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField</name>
              <fields>
                <field>
                  <name>consumerIndex</name>
                  <allowUnsafeAccess>true</allowUnsafeAccess>
                </field>
              </fields>
            </reflection>
            <reflection>
              <name>io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields</name>
              <fields>
                <field>
                  <name>producerIndex</name>
                  <allowUnsafeAccess>true</allowUnsafeAccess>
                </field>
              </fields>
            </reflection>
            <reflection>
              <name>io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields</name>
              <fields>
                <field>
                  <name>producerLimit</name>
                  <allowUnsafeAccess>true</allowUnsafeAccess>
                </field>
              </fields>
            </reflection>
            <reflection>
              <name>io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields</name>
              <fields>
                <field>
                  <name>consumerIndex</name>
                  <allowUnsafeAccess>true</allowUnsafeAccess>
                </field>
              </fields>
            </reflection>
          </reflections>

(I'm using Geronimo Arthur as graalvm wrapper).

Side note I'm using 21.0.0.2.r11 of graalvm and only change is a netty upgrade.

@jvican
Copy link

jvican commented Jun 9, 2021

FWIW it looks like -Dio.netty.noUnsafe is not necessary to get a performant Netty-based application compiled by GraalVM. I've been using this flag in production and recently discovered that I could remove it and Netty would behave just fine.

absurdfarce added a commit to absurdfarce/graal-cassandra-driver that referenced this issue Jun 23, 2021
…g the native app.

These were likely brought on by the Netty upgrade... see netty/netty#10376
for additional details.
luca-digrazia pushed a commit to luca-digrazia/DatasetCommitsDiffSearch that referenced this issue Sep 4, 2022
…ively and the unsafe configuration must be adjusted.

    Follow up of the discussion from netty/netty#10376
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants