Hello Alan,

On 19/10/21 7:40 pm, Alan Bateman wrote:
On 19/10/2021 14:49, Jaikiran Pai wrote:

Ah! So this exact same investigation had already happened a few weeks back then. I haven't subscribed to that list, so missed it. I see in one of those messages this part:

"Off hand I can't think of any issues with the ModuleDescriptor hashCode. It is computed at link time and should be deterministic. If I were to guess then then this may be something to do with the module version recorded at compile-time at that is one of the components that the hash is based on."

To be clear, is the ModuleDescriptor#hashCode() expected to return reproducible (same) hashCode across multiple runs? What currently changes the hashCode() across multiple runs is various components within ModuleDescriptor's hashCode() implementation using the hashCode() of the enums (specifically the various Modifier enums).

The discussion on jigsaw-dev didn't get to the bottom of the issue at the time, mostly because it wasn't easy to reproduce.

Now that the issue is clearer then we should fix it. Aside from reproducible builds then I expect it is possible to use a ModuleDescriptor.Builder to create a ModuleDescriptor that is equal to a ModuleDescriptor in boot layer configuration but with a different hashCode.

Based on this input, one of the tests I have included for verifying this proposed hashCode fix, involves dealing with a boot layer ModuleDescriptor and then verifying it's hashCode against a ModuleDescriptor that is built for the same module using the ModuleDescriptor.Builder. It does reproduce the hashCode issue. However, that test seems to have exposed a different bug with CDS and equality checks against enums (which impact ModuleDescriptor#equals()).

More precisely, consider this trivial Java code:

import java.lang.module.*;
import java.io.*;
import java.util.*;

public class EnumEquality {

    public static void main(final String[] args) throws Exception {
        String moduleName = "java.sql";
        // load the "java.sql" module from boot layer
        Optional<Module> bootModule = ModuleLayer.boot().findModule(moduleName);
        if (bootModule.isEmpty()) {
            throw new RuntimeException(moduleName + " module is missing in boot layer");
        }
        ModuleDescriptor m1 = bootModule.get().getDescriptor();
        // now recreate the same module using the ModuleDescriptor.read on the module's module-info.class
        ModuleDescriptor m2;
        try (InputStream moduleInfo = bootModule.get().getResourceAsStream("module-info.class")) {
            if (moduleInfo == null) {
                throw new RuntimeException("Could not locate module-info.class in " + moduleName + " module");
            }
            // this will internally use a ModuleDescriptor.Builder to construct the ModuleDescriptor
            m2 = ModuleDescriptor.read(moduleInfo);
        }
        if (!m1.equals(m2)) {
            // root cause - the enums aren't "equal"
            for (ModuleDescriptor.Requires r1 : m1.requires()) {
                if (r1.name().equals("java.base")) {
                    for (ModuleDescriptor.Requires r2 : m2.requires()) {
                        if (r2.name().equals("java.base")) {
                            System.out.println("Modifiers r1 " + r1.modifiers() + " r2 " + r2.modifiers()                                 + " --> equals? " + r1.modifiers().equals(r2.modifiers()));
                        }
                    }
                }
            }

            throw new RuntimeException("ModuleDescriptor(s) aren't equal: \n" + m1 + "\n" + m2);
        }
        System.out.println("Success");
    }
}

This program uses "java.sql" as the module under test. This "java.sql" is a boot layer module and among other things has:

    requires transitive java.logging;
    requires transitive java.transaction.xa;
    requires transitive java.xml;

in its module definition[1]. The program first loads this module's ModuleDescriptor into an instance m1, using the boot() module layer. It then "reconstructs" this same module by reading the module-info.class of this module, using the ModuleDescriptor.read() API (which internally calls ModuleDescriptor.Builder). This is stored into m2. m1 and m2 are then checked for equality (using a call to equals() method). This equality check keeps failing consistently.

Digging into it, it appears that since the ModuleDescriptor#equals() calls equals() on enum types (in this specific case on ModuleDescriptor.Requires.Modifier) and since enum type equality is implemented as identity checks, those identity checks are surprisingly failing. More specifically ModuleDescriptor.Requires.Modifier.MANDATED == ModuleDescriptor.Requires.Modifier.MANDATED is equating to false because at runtime I see that two different instances of ModuleDescriptor.Requires.Modifier.MANDATED have been loaded (by the same boot module classloader). Although I use ModuleDescriptor.Requires.Modifier.MANDATED as an example, the same is reproducible with other enum values like ModuleDescriptor.Requires.Modifier.TRANSITIVE.

This appears to be specific to CDS since running the above program with:

java -Xshare:off EnumEquality

succeeds and the ModuleDescriptor equality check passes.

In short, it looks like there is some general issue with CDS and equality checks with enums and perhaps deserves a separate JBS issue?

[1] https://github.com/openjdk/jdk/blob/master/src/java.sql/share/classes/module-info.java#L34

-Jaikiran


Reply via email to