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

GlassFish modules cleaning and some fixes #6596

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

pepness
Copy link
Member

@pepness pepness commented Oct 19, 2023

NetBeans GlassFish module notes:

  • Use ConcurrentHashMap insted of Collections.synchronizedMap
  • Use ConcurrentHashMap.newKeySet instead of Collections.synchronizedMap
  • When using ConcurrentHashMap remove some synchronized blocks when using
    atomic and concurrent operations (remove, put, containsKey, and get).
  • Refactor to computeIfAbsent pattern when possible.
  • Add javadoc support for JAKARTA EE 9 and JAKARTA EE 10
  • Add missing validations for Jakarta EE 10
  • Close resource with try-with-resources
  • Remove redundant calls to si.getDeployerUri() and this.getClass()
  • Remove redundant call to some methods getConfigRoot(), getDeployerUri()
  • Use GlassFishLogger instead of generic glassfish-ee logger
  • Use target class for logger instead of "glassfish"
  • Regen sig file because ant check-sigtests-release failed
  • Fix path to javadocs when creating a new ConfigBuilder
  • Make final some class instances
  • Refactor method versionToResourceFilesIndexes()
  • Fix some javadoc comments
  • Make final some class instances
  • Add imports and remove commented code
  • Add TODO comments
  • Change inefficient creation of new Array
  • Remove commented code
  • Fix some typos

NetBeans Testing:

  • Verify successful execution of libraries and licenses Ant test
  • Verify successful execution of Verify Sigtests
  • Verify successful execution of ant -Dcluster.config=release commit-validation
  • Verify successful execution of unit tests for modules glassfish.common, glassfish.javaee, glassfish.tooling,
    and glassfish.eecommon
  • Started NetBeans and ensure the log didn't have any ERROR or new WARNINGS
  • Successfully register GlassFish versions 4, 5, 5.1, 6, 6.2, and 7. Then create a web app for each version
    and verify that they work.

-Remove commented code
-Make some instance fields final
-Fix some typos
-Add some imports
 atomic and concurrent operations (remove, put, containsKey and get).
-Remove redundant call to some methods getConfigRoot(), getDeployerUri()
 atomic and concurrent operations (remove, put, and get).
-Use ConcurrentHashMap.newKeySet instead of Collections.synchronizedMap
-Remove redundant calls to si.getDeployerUri() and this.getClass()
-Add a TODO comment
-Change inefficient creation of new Array
 atomic and concurrent operations (remove, put, and get).
-Close resource with try-with-resources
-Add imports and remove commented code
  atomic and concurrent operations
- Add javadoc code support for JAKARTA EE 9 and JAKARTA EE 10
- Initialize `serverVersion` and `builder` inside the constructor
- Use `GlassFishLogger` instead of generic `glassfish-ee` logger
- Make final some class instances
@pepness pepness added Java EE/Jakarta EE [ci] enable enterprise job enterprise [ci] enable enterprise job labels Oct 19, 2023
@pepness pepness added this to the NB21 milestone Oct 19, 2023
@pepness pepness self-assigned this Oct 19, 2023
Copy link
Contributor

@matthiasblaesing matthiasblaesing left a comment

Choose a reason for hiding this comment

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

I found some issues, that should be fixed and I think the changes moving from a HashMap with external synchonization to a ConcurrentHashMap is wrong at least in the commented areas. I think I also saw an instance where now synchronization is invoked on a ConcurrentHashMap, which might or might not work depending on the implementation of the Map.
Oh and we need to hope that noone wanted to store a value with the key being null. With a HashMap that works, a ConcurrentHashMap will blow.

Comment on lines +95 to +96
public static final String PRELUDE_DEFAULT_NAME = "GlassFish_v3_Prelude"; //NOI18N
public static final String EE6WC_DEFAULT_NAME = "GlassFish_Server_3.1"; // NOI18N
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the signature file and if I remember corretly, this changes the semantic of the two fields. Before the change users had to lookup the value from the fields at runtime and thus when linking with a newer module, would get updated values. This now being private static final requires all modules to be rebuild when the value changes. I would advice against that. A comment mentioning that might be helpful.

Copy link
Member Author

Choose a reason for hiding this comment

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

The fields are public, will this also require all modules to be rebuild?

Copy link
Contributor

Choose a reason for hiding this comment

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

The problem is the "final static". It took me some digging to find the specificiation basis and not just some random note, but this builds it:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4:

A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28). Whether a variable is a constant variable or not may have implications with respect to class initialization (§12.4.1), binary compatibility (§13.1, §13.4.9), and definite assignment (§16 (Definite Assignment)).

https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.1 (section 3)

A reference to a field that is a constant variable (§4.12.4) must be resolved at compile time to the value V denoted by the constant variable's initializer.

If such a field is static, then no reference to the field should be present in the code in a binary file, including the class or interface which declared the field. Such a field must always appear to have been initialized (§12.4.2); the default initial value for the field (if different than V) must never be observed.

You can observe this with this project: constant_variable.zip

Unzip and run from the base directory:

mvn clean process-classes; javap -v -p target/classes/eu/doppelhelix/dev/constantvariable/UseSite.class

You see, that the string is evaluated at compile time. The string is loaded from the constant table (ldc call) an directly passed to println from System.out. The output was created by OpenJDK 17 as shipped with Ubuntu, it might be different in your case.

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #15                 // String Hello World!
         5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;

Now open ConstantProvider.java, comment out "Variant 1" and choose one of the other variants, repeat.

This yields a much more complex result:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: getstatic     #13                 // Field eu/doppelhelix/dev/constantvariable/ConstantProvider.WORLD:Ljava/lang/String;
         6: invokedynamic #19,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
        11: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
      LineNumberTable:
        line 7: 0
        line 8: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
}
SourceFile: "UseSite.java"
BootstrapMethods:
  0: #43 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #49 Hello \u0001!
InnerClasses:
  public static final #56= #52 of #54;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

Now the value of ConstantProvider.WORLD is loaded first (get static #13) and then passed to the invoke dynamic, that then builds the string at runtime.

TL;DR version: Move initialization into a static initializer block and the variables can become final, which being inlined.

@@ -97,7 +99,7 @@ public Props(Map<String, String> map) {
if (map == null) {
throw new NullPointerException("Source Map shall not be null.");
}
this.delegate = map;
this.delegate = new ConcurrentHashMap<>(map);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is a good change, as the documentation implies a new map is constructed, but is this a safe assumption? I mean that before this change, a caller could modify the Props through the map provided here.


logViewMgr = instances.computeIfAbsent(uri, key ->
new WeakReference<LogViewMgr>(new LogViewMgr(uri))
).get();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is wrong. The value is a WeakReference. Line 140 in the old code fetch the referenced objects, effectively creating a new hardlink for it. If that fails (get returns null), then a new instance is created and a new WeakReference is created.
The supplier fed into ConcurrentMap#computeIfAbsent will only be invoked, if the WeakReference object is removed, which is not what was checked before.

Copy link
Member Author

Choose a reason for hiding this comment

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

The supplier will be executed if the key is absent in the map.

I added some logging to this method and test both versions and the behavior is the same, this method triggers when starting, restarting, and stopping the server or when selecting the option View Domain Server Log:

Screenshot from 2023-10-21 19-25-36

The first time it runs it will:

  1. search the key(uri) in the map, because is the first time it runs with this GlassFish instance it will return null
  2. compute the value to be inserted with the provided key (uri)
  3. insert the [key, value] entry into the map
  4. return the value which is a WeakReference
  5. execute the .get() method from the returned value
  6. assign the LogViewMgr obtained in step 5
  7. return the LogViewMgr object
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: uri: [/path/to/app/server/glassfish625/glassfish:/path/to/app/server/glassfish625/glassfish/domains/domain1]deployer:gfv610ee9:localhost:4848
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: instances.get(uri): null
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: instances.get(uri).get(): null
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: instances.get(uri): java.lang.ref.WeakReference@766adea5
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: logViewMgr: org.netbeans.modules.glassfish.common.LogViewMgr@766adea3

The subsequent calls to the same GlassFish instance will log the same:

  1. search the key(uri) in the map, it exists in the map
  2. return the value associated to the key, a WeakReference object
  3. execute the .get() method from the returned value
  4. assign the LogViewMgr obtained in step 3
  5. return the LogViewMgr object
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: uri: [/path/to/app/server/glassfish625/glassfish:/path/to/app/server/glassfish625/glassfish/domains/domain1]deployer:gfv610ee9:localhost:4848
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: instances.get(uri): java.lang.ref.WeakReference@766adea5
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: instances.get(uri).get(): org.netbeans.modules.glassfish.common.LogViewMgr@766adea3
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: instances.get(uri): java.lang.ref.WeakReference@766adea5
INFO [org.netbeans.modules.glassfish.common.LogViewMgr]: logViewMgr: org.netbeans.modules.glassfish.common.LogViewMgr@766adea3

I tested with two different GlassFish instances (version 6 and 7).

Copy link
Contributor

Choose a reason for hiding this comment

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

Testing does not help when reasoning about possible synchronisation issues. They have the tendency not to manifest in tests and show in real life. The argument must be based on what is guaranteed by the language.

With this change the following can happen:

  1. Call to LogViewMgr#getInstance with a fixed URI uri1 happens. There is no key for uri1 in the map instances and this the supplier is invoked. It creates a new LogViewMgr, wraps it into a WeakReference, places it in instances and returns it to the caller, which unwraps it using WeakReference#get and returns it.
  2. The system does something with the LogViewMgr instance and at some point removes the last hard reference to it
  3. Garbage collections happens
  4. LogViewMgr#getInstance with a fixed URI uri1 is called again. Now there is a WeakReference for the key uri1, which is returned and WeakReference#get is invoked on that. But because (3) happend you now get null. The code is broken.

There is another race in step 1. To better see this, the code can be split to:

    public static LogViewMgr getInstance(String uri) {
        WeakRef<LogViewMgr> logViewMgrRef = instances.computeIfAbsent(uri, key -> 
                        new WeakReference<LogViewMgr>(new LogViewMgr(uri))
                    );
       LogViewMgr logViewMgr = logViewMgrRef.get();
        
        return logViewMgr;
    }

There is a very short window where the instance created in the supplier is not hard referenced anymore. After it is created and before logViewMgrRef.get() it is possible, that GC happens and the instance is collected. logViewMgrRef.get() will then yield null.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, both these things need fixing. Using new in the arguments to a WeakReference should always be considered an anti-pattern. I mean, the language doesn't even guarantee not to garbage collect an object before the constructor has finished! 😉 The strong reference needs to be held, and the reference needs checking on every retrieval.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for the explanation, the code example makes it easier to understand.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will this fix the hard reference issue?

public static LogViewMgr getInstance(String uri) {
    LogViewMgr logViewMgr;
    logViewMgr = instances.computeIfAbsent(uri, (String key) -> {
                LogViewMgr _logViewMgr = new LogViewMgr(uri);
                WeakReference<LogViewMgr> _viewRef = new WeakReference<>(_logViewMgr);
                return _viewRef;
            }).get();
return logViewMgr;

Where is the race problem @matthiasblaesing? The ConcurrentHashMap implementation of computeIfAbsent is atomic and concurrent.
ConcurrentHashMap.computeIfAbsent

The entire method invocation is performed atomically. The supplied function is invoked exactly once per invocation of this method if the key is absent, else not at all. Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple.

This will be executed in a synchronized block at a Node level instead of synchronizing the entire map.

Copy link
Member

Choose a reason for hiding this comment

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

Will this fix the hard reference issue?

No. That pretty much amounts to the same code.

This will be executed in a synchronized block at a Node level instead of synchronizing the entire map.

Is there evidence that's a bottleneck?

Copy link
Contributor

Choose a reason for hiding this comment

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

Where is the race problem @matthiasblaesing? The ConcurrentHashMap implementation of computeIfAbsent is atomic and concurrent.

There are two races, so I don't know which of the two needs more explanation than what was provided.

Copy link
Member Author

Choose a reason for hiding this comment

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

@neilcsmith-net I agree there is no bottleneck, just using a function that didn't fit the code pattern.

@matthiasblaesing After a long week, now I see the problem: the .get() is out of the safe synchronized block, my bad. Thank you both for the time and explanations.

Comment on lines 1124 to 1126
FetchLog storedLog = serverInputStreams.computeIfPresent(instance, (key, value) -> value);
if (this.log.equals(storedLog)) {
oldLog = serverInputStreams.remove(instance);
Copy link
Contributor

Choose a reason for hiding this comment

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

Changes semantics(race condition). Before operations in lines 1118-1121 were atomic, after a different thread might change serverInputStreams and/or oldLog while the lines are executed.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are correct.

removeLog(instance);
}
} else {
log = serverInputStreams.computeIfPresent(instance, (key, value) -> value);
Copy link
Contributor

Choose a reason for hiding this comment

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

Again changed semantics. Code that was atomic before is not anymore.

Copy link
Member Author

Choose a reason for hiding this comment

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

Again, you are correct. Thanks for the observations.

if (null != catalog) {
catalog.refreshRunTimeDDCatalog(this, si.getGlassfishRoot());
}
try {
Copy link
Contributor

Choose a reason for hiding this comment

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

The code makes assumptions about sychronisation (see line 268 in new code, line 258 in old code). As the synchronisation was removed, between line 264 (new code) and line 268 (new code) the map might be modified and the the following code is not executed.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are correct, it needs to be synchronized.

Comment on lines -217 to +212
synchronized(delegate) {return delegate.get(key);}
return delegate.computeIfPresent((String)key, (k, v) -> v);
Copy link
Member

Choose a reason for hiding this comment

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

same here, this is a get()

Copy link
Member Author

Choose a reason for hiding this comment

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

The same reason, synchronized just a Node instead of the entire map.

Copy link
Member

Choose a reason for hiding this comment

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

please elaborate. Why is this here needed and in java.util.Properties().get() not?

same question for enterprise/glassfish.common/src/org/netbeans/modules/glassfish/common/GlassfishInstanceProvider.java

@mbien
Copy link
Member

mbien commented Oct 28, 2023

what anticipated performance issue is the rewrite in CHM supposed to improve? The access pattern looks like there is probably not much contention happening. Once the instances are in the map, the read operations should be really fast while using synchronization since the critical sections are tiny.

the refactoring does also not get rid of many critical sections, e.g:

        public boolean isEmpty() {
            synchronized(delegate) {return delegate.isEmpty();}
        }

so there would be more work left to finish it.

I usually would see concurrency updates a bit outside the scope of cleanups. Is it worth spending the extra time in review for risky changes when the access pattern is ending up to be get() after short initialization?

@neilcsmith-net neilcsmith-net modified the milestones: NB21, NB22 Jan 16, 2024
@ebarboni ebarboni modified the milestones: NB22, NB23 Apr 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enterprise [ci] enable enterprise job Java EE/Jakarta EE [ci] enable enterprise job
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants