blackdrag commented on code in PR #2820:
URL: https://github.com/apache/groovy/pull/2820#discussion_r3821836699


##########
src/main/java/org/apache/groovy/runtime/indy/IndyInvalidation.java:
##########


Review Comment:
   The architectural relationship between `IndyInvalidation` and 
`SwitchPointInvalidator` makes me wonder. The issue is not only that 
`IndyInvalidation` is tightly coupled to `SwitchPointInvalidator`, but also 
which of the two is supposed to be the abstraction boundary?
   
   * If `IndyInvalidation` is a wrapper/facade around `SwitchPointInvalidator`, 
then `SwitchPointInvalidator` is effectively an implementation detail of the 
higher-level invalidation mechanism. In that case, it would be surprising for 
`SwitchPointInvalidator` to remain independently exposed and used. Consumers 
should normally go through `IndyInvalidation`, otherwise the wrapper does not 
really define the abstraction boundary.
   
   * If `SwitchPointInvalidator` is intended to be an independently usable 
abstraction, then the two classes should be considered independent components 
which happen to cooperate. In that case, their respective responsibilities, 
lifecycle guarantees and invariants need to be clearly defined. In particular, 
it should be clear which guarantees `SwitchPointInvalidator` provides on its 
own and which only exist when it is managed through `IndyInvalidation`.
   
   At the moment, the implementation seems somewhat in between these two 
models. `IndyInvalidation` directly manages `SwitchPointInvalidator` instances 
and depends on its lifecycle and global state, which makes the relationship 
look more like a layered abstraction than two independent components. At the 
same time, `SwitchPointInvalidator` is still exposed and can be used 
independently.
   
   This should be document, otherwise it is difficult to determine if the 
current coupling is intentional or simply an implementation dependency that has 
become externally visible.
   
   If `SwitchPointInvalidator` can be used independently as well as through 
`IndyInvalidation`, then the correctness of the overall invalidation machinery 
cannot simply rely on invariants established by `IndyInvalidation`. The 
independent use cases need to obey the same assumptions, or the boundary 
between the two needs to enforce those assumptions.
   
   If `SwitchPointInvalidator` is really only the low-level mechanism behind 
`IndyInvalidation`, hiding it behind that abstraction would make the ownership 
and lifecycle model much easier to reason about and would also leave room to 
change the underlying invalidation mechanism later.
   
   I think the important point is not simply whether the classes are coupled, 
but whether the current API and usage actually match the intended abstraction 
boundary. If they are one layered subsystem, that relationship should be 
explicit; if they are independent abstractions, their independence and 
invariants need to be justified.
   



##########
src/main/java/org/codehaus/groovy/reflection/ClassInfo.java:
##########
@@ -110,6 +112,29 @@ public class ClassInfo implements Finalizable {
     private static final ManagedConcurrentLinkedQueue<ClassInfo> 
modifiedExpandos =
             new ManagedConcurrentLinkedQueue<ClassInfo>(weakBundle);
 
+    /**
+     * Whether {@link #globalClassValue} stores its values softly with
+     * resurrection ({@code -Dgroovy.use.classvalue=soft}, GROOVY-12281
+     * investigation prototype). Soft mode needs two cooperating pieces here:
+     * strong roots for non-reconstructible state ({@link 
#nonReclaimableRoots})
+     * and per-Class indy domain continuity (see {@link #indyDomain()}).
+     */
+    private static final boolean SOFT_CLASS_VALUES = 
GroovyClassValueFactory.isSoftMode();

Review Comment:
   This isSoftMode() is a bit suspicious. Again it produces a tight coupling to 
a certain implementation. Why is a ClassValue with SoftReferences a soft mode, 
but a ManagedMap based version not?



##########
src/main/java/org/codehaus/groovy/reflection/GroovyClassValueFactory.java:
##########
@@ -33,13 +33,30 @@ class GroovyClassValueFactory {
         */
        private static final String CLASSVALUE_MODE = 
SystemUtil.getSystemPropertySafe("groovy.use.classvalue", "true");
 
+       /**
+        * GROOVY-12281 investigation prototype: whether values are stored 
behind
+        * {@link java.lang.ref.SoftReference}s with resurrection semantics
+        * ({@code -Dgroovy.use.classvalue=soft}). Soft mode needs cooperation 
from
+        * {@link ClassInfo} (strong roots for non-reconstructible state, 
per-Class
+        * indy domain continuity), which is why it is exposed package-wide 
rather
+        * than kept local to {@link #createGroovyClassValue}.
+        */
+       static boolean isSoftMode() {
+               return "soft".equalsIgnoreCase(CLASSVALUE_MODE);

Review Comment:
   why not store soft mode as boolean?



##########
src/main/java/org/codehaus/groovy/reflection/ClassInfo.java:
##########
@@ -110,6 +112,29 @@ public class ClassInfo implements Finalizable {
     private static final ManagedConcurrentLinkedQueue<ClassInfo> 
modifiedExpandos =
             new ManagedConcurrentLinkedQueue<ClassInfo>(weakBundle);
 
+    /**
+     * Whether {@link #globalClassValue} stores its values softly with
+     * resurrection ({@code -Dgroovy.use.classvalue=soft}, GROOVY-12281
+     * investigation prototype). Soft mode needs two cooperating pieces here:
+     * strong roots for non-reconstructible state ({@link 
#nonReclaimableRoots})
+     * and per-Class indy domain continuity (see {@link #indyDomain()}).
+     */
+    private static final boolean SOFT_CLASS_VALUES = 
GroovyClassValueFactory.isSoftMode();
+
+    /**
+     * Soft-mode strong roots for ClassInfos whose state could not be
+     * reconstructed if the instance were soft-collected and later recreated:
+     * an installed class-level MetaClass ({@link #setStrongMetaClass}),
+     * per-instance MetaClasses, or registry-written DGM/extension method
+     * arrays ({@link CachedClass#setNewMopMethods}/{@code addNewMopMethods}).
+     * The set itself is Groovy-loaded, so it contributes no foreign-loader
+     * pinning: everything in it dies with Groovy's own loader, which is the
+     * lifetime every ClassInfo has today under the default strong ClassValue.
+     * {@code null} unless soft mode is active.
+     */
+    private static final Set<ClassInfo> nonReclaimableRoots =
+            SOFT_CLASS_VALUES ? ConcurrentHashMap.newKeySet() : null;

Review Comment:
   What happens if we always do that and not only in soft mode? This is also 
not the only place. Either we have to do it, then this is missing an 
abstraction in my eyes. Or we do not, well, then we should simplify the code



##########
src/main/java/org/codehaus/groovy/reflection/GroovyClassValueSoft.java:
##########
@@ -0,0 +1,149 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.reflection;
+
+import org.apache.groovy.util.concurrent.ConcurrentReferenceHashMap;
+import org.apache.groovy.util.concurrent.ConcurrentReferenceHashMap.Option;
+import 
org.apache.groovy.util.concurrent.ConcurrentReferenceHashMap.ReferenceType;
+
+import java.lang.ref.SoftReference;
+import java.util.EnumSet;
+
+/**
+ * GROOVY-12281 investigation prototype ({@code -Dgroovy.use.classvalue=soft}):
+ * keeps {@code java.lang.ClassValue}'s per-{@code Class} fast path but stores
+ * each value behind a {@link SoftReference}, so an association on an immortal
+ * platform class (for example {@code String}) no longer holds a strong chain
+ * to the value's class loader (JDK-8136353 / GROOVY-12142). The stored wrapper
+ * is a bootstrap-loaded {@code java.lang.ref.SoftReference}, and the per-Class
+ * entry references the defining {@code ClassValue} only weakly (via its
+ * {@code Version}; the map key is a bootstrap {@code Identity} object), so the
+ * only path from an immortal key to the Groovy-loaded value is softly
+ * reachable and is cleared under memory pressure once nothing else keeps the
+ * Groovy island alive.
+ * <p>
+ * Correctness relies on <em>resurrection</em>: a weak-key/weak-value side map
+ * is the identity authority. {@code computeValue} consults it first, so when
+ * the {@code ClassValue}'s soft reference has been cleared but the value is
+ * still reachable anywhere — for example captured by a linked call site — the
+ * same instance is re-associated rather than a fresh one created. A fresh
+ * instance can only be created once the old one is weakly unreachable, at
+ * which point no guard, cache or call site can still observe the old
+ * instance, so "two live generations of one association" cannot arise.
+ * <p>
+ * {@link #remove(Class)} stays a <em>hard detach</em> (the identity entry is
+ * purged too), preserving the documented undeploy semantics of
+ * {@link ClassInfo#remove(Class)}: the next {@code get} creates a fresh value
+ * even if the old one is still reachable somewhere.
+ *
+ * @param <T> the value type
+ */
+class GroovyClassValueSoft<T> implements GroovyClassValue<T> {
+
+    private final ComputeValue<T> computeValue;
+
+    /**
+     * Identity authority: weak identity keys (no key class is pinned by this
+     * map) and weak values (the map keeps nothing alive by itself; it only
+     * remembers a value for as long as something else does).
+     */
+    private final ConcurrentReferenceHashMap<Class<?>, T> canonical =
+            new ConcurrentReferenceHashMap<>(ReferenceType.WEAK, 
ReferenceType.WEAK,
+                    EnumSet.of(Option.IDENTITY_COMPARISONS));
+
+    /**
+     * Striped locks for the canonical-miss path. Creation must be mutually
+     * exclusive per key: without it two racing threads could each create and
+     * leak a distinct instance through the uncached fallback in
+     * {@link #get(Class)}, breaking the identity guarantee the side map
+     * exists to provide. (The map's own {@code putIfAbsent} cannot be used
+     * for this: on an entry whose collected value has not yet been purged it
+     * reports success without storing the replacement.)
+     */
+    private final Object[] creationLocks = new Object[64];
+
+    private final ClassValue<SoftReference<T>> store = new 
ClassValue<SoftReference<T>>() {
+        @Override
+        protected SoftReference<T> computeValue(final Class<?> type) {
+            return new SoftReference<>(canonical(type));

Review Comment:
   I actually no wonder... does the ClassInfo case really need a SoftReference 
or could it be a WeakReference? The SoftReference is maybe the more "general", 
"cache friendly" variant, but I have now second thoughts of if it should be 
defined at this level, also considering the factory. But it could stay like 
this for now



##########
src/main/java/org/codehaus/groovy/reflection/GroovyClassValueFactory.java:
##########
@@ -33,13 +33,30 @@ class GroovyClassValueFactory {
         */
        private static final String CLASSVALUE_MODE = 
SystemUtil.getSystemPropertySafe("groovy.use.classvalue", "true");
 
+       /**
+        * GROOVY-12281 investigation prototype: whether values are stored 
behind
+        * {@link java.lang.ref.SoftReference}s with resurrection semantics
+        * ({@code -Dgroovy.use.classvalue=soft}). Soft mode needs cooperation 
from
+        * {@link ClassInfo} (strong roots for non-reconstructible state, 
per-Class
+        * indy domain continuity), which is why it is exposed package-wide 
rather
+        * than kept local to {@link #createGroovyClassValue}.
+        */
+       static boolean isSoftMode() {
+               return "soft".equalsIgnoreCase(CLASSVALUE_MODE);
+       }
+
        public static <T> GroovyClassValue<T> 
createGroovyClassValue(ComputeValue<T> computeValue) {
-               // GROOVY-12281 investigation prototype: "hybrid" routes 
platform-loader keys to the
+               // GROOVY-12281 investigation prototypes: "hybrid" routes 
platform-loader keys to the
                // weak-key map and everything else to ClassValue, so immortal 
platform keys never
-               // pin the value's loader while user classes keep the per-class 
fast path.
+               // pin the value's loader while user classes keep the per-class 
fast path (measured,
+               // declined); "soft" keeps ClassValue for all keys but holds 
values softly with
+               // resurrection, so immortal keys hold no strong chain to the 
value's loader.
                if ("hybrid".equalsIgnoreCase(CLASSVALUE_MODE)) {

Review Comment:
   not part of the PR really, but if you do a boolean for soft mode, you could 
also do one for hybrid then. Or you switch to a model where you do not use the 
boolean, but a provider GroovyClassValueHybrid::new and so on. Just a thought



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to