This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-graalvm-distro.git


The following commit(s) were added to refs/heads/main by this push:
     new f5d9748  Replace HierarchyDefinitionService GroovyShell with 
Java-backed closures
f5d9748 is described below

commit f5d97485b1e5da5a575ef75c79dbb5a10c1c0c2e
Author: Wu Sheng <[email protected]>
AuthorDate: Fri Feb 20 16:54:54 2026 +0800

    Replace HierarchyDefinitionService GroovyShell with Java-backed closures
    
    HierarchyDefinitionService.MatchingRule used GroovyShell.evaluate() to
    compile 4 closure expressions from hierarchy-definition.yml at runtime.
    This blocks GraalVM native image where dynamic Groovy compilation is
    unavailable.
    
    Replace with same-FQCN class in server-core-for-graalvm that provides
    pre-built Closure<Boolean> implementations in Java, keyed by rule name.
    Unknown rule names fail fast at startup. HierarchyService (the caller)
    is unchanged — MatchingRule.getClosure() still returns Closure<Boolean>.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 DISTRO-POLICY.md                                   |   7 +-
 .../server-core-for-graalvm/pom.xml                |   2 +
 .../core/config/HierarchyDefinitionService.java    | 193 +++++++++++++++++++++
 3 files changed, 199 insertions(+), 3 deletions(-)

diff --git a/DISTRO-POLICY.md b/DISTRO-POLICY.md
index 000cd9f..4e53997 100644
--- a/DISTRO-POLICY.md
+++ b/DISTRO-POLICY.md
@@ -133,13 +133,13 @@ Each upstream JAR that has replacement classes gets a 
corresponding `*-for-graal
 
 `oap-graalvm-server` depends on `*-for-graalvm` JARs instead of originals. 
Original upstream JARs are forced to `provided` scope via 
`<dependencyManagement>` to prevent transitive leakage.
 
-### 18 Same-FQCN Replacement Classes Across 12 Modules
+### 19 Same-FQCN Replacement Classes Across 12 Modules
 
 **Non-trivial replacements (load pre-compiled assets from manifests):**
 
 | Module | Replacement Classes | Purpose |
 |---|---|---|
-| `server-core-for-graalvm` | `OALEngineLoaderService`, `AnnotationScan`, 
`SourceReceiverImpl`, `MeterSystem`, `CoreModuleConfig` | Load from manifests 
instead of Javassist/ClassPath; config with @Setter |
+| `server-core-for-graalvm` | `OALEngineLoaderService`, `AnnotationScan`, 
`SourceReceiverImpl`, `MeterSystem`, `CoreModuleConfig`, 
`HierarchyDefinitionService` | Load from manifests instead of 
Javassist/ClassPath; config with @Setter; Java-backed closures instead of 
GroovyShell |
 | `library-util-for-graalvm` | `YamlConfigLoaderUtils` | Set config fields via 
setter instead of reflection |
 | `meter-analyzer-for-graalvm` | `DSL`, `FilterExpression` | Load pre-compiled 
MAL Groovy scripts from manifest |
 | `log-analyzer-for-graalvm` | `DSL`, `LogAnalyzerModuleConfig` | Load 
pre-compiled LAL scripts; config with @Setter |
@@ -295,7 +295,8 @@ and ~1254 Groovy scripts stored in JARs.
 - [ ] `resource-config.json` for runtime config files loaded via 
`ResourceUtils.read()`
 - [ ] Configure gRPC/Netty/Protobuf for native image
 - [ ] GraalVM Feature class for SkyWalking-specific registrations
-- [ ] Resolve remaining code-level blockers: OTEL SPI, Envoy SPI, 
HierarchyDefinitionService Groovy
+- [x] Resolve HierarchyDefinitionService Groovy blocker (same-FQCN replacement 
with Java-backed closures)
+- [ ] Verify OTEL and Envoy ServiceLoader SPI work in native image (GraalVM 
supports META-INF/services natively)
 - [ ] Get OAP server booting as native image with BanyanDB
 
 ### Phase 4: Harden & Test
diff --git a/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml 
b/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml
index ba898f4..deba20d 100644
--- a/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml
+++ b/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml
@@ -64,6 +64,8 @@
                                 
<exclude>org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.class</exclude>
                                 
<exclude>org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.class</exclude>
                                 
<exclude>org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem$*.class</exclude>
+                                
<exclude>org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.class</exclude>
+                                
<exclude>org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService$*.class</exclude>
                             </excludes>
                         </filter>
                     </filters>
diff --git 
a/oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.java
 
b/oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.java
new file mode 100644
index 0000000..a946708
--- /dev/null
+++ 
b/oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.java
@@ -0,0 +1,193 @@
+/*
+ * 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.apache.skywalking.oap.server.core.config;
+
+import groovy.lang.Closure;
+import java.io.FileNotFoundException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.CoreModuleConfig;
+import org.apache.skywalking.oap.server.core.UnexpectedException;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.query.type.Service;
+import org.apache.skywalking.oap.server.library.util.ResourceUtils;
+import org.yaml.snakeyaml.Yaml;
+
+import static java.util.stream.Collectors.toMap;
+
+/**
+ * Same-FQCN replacement of upstream HierarchyDefinitionService.
+ *
+ * <p>Replaces {@code GroovyShell.evaluate()} in {@code MatchingRule} with
+ * pre-built Java-backed {@link Closure} objects. Eliminates runtime Groovy
+ * compilation, which is not available in GraalVM native image.
+ *
+ * <p>The 4 matching rules from {@code hierarchy-definition.yml} are 
implemented
+ * as anonymous {@code Closure<Boolean>} subclasses in {@link #RULE_REGISTRY}.
+ * Unknown rule names fail fast at startup.
+ */
+@Slf4j
+public class HierarchyDefinitionService implements 
org.apache.skywalking.oap.server.library.module.Service {
+
+    /**
+     * Pre-built matching rule closures keyed by rule name from 
hierarchy-definition.yml.
+     * Each closure takes two Service arguments (upper, lower) and returns 
Boolean.
+     */
+    private static final Map<String, Closure<Boolean>> RULE_REGISTRY;
+
+    static {
+        RULE_REGISTRY = new HashMap<>();
+
+        // name: "{ (u, l) -> u.name == l.name }"
+        RULE_REGISTRY.put("name", new Closure<Boolean>(null) {
+            public Boolean doCall(final Service u, final Service l) {
+                return Objects.equals(u.getName(), l.getName());
+            }
+        });
+
+        // short-name: "{ (u, l) -> u.shortName == l.shortName }"
+        RULE_REGISTRY.put("short-name", new Closure<Boolean>(null) {
+            public Boolean doCall(final Service u, final Service l) {
+                return Objects.equals(u.getShortName(), l.getShortName());
+            }
+        });
+
+        // lower-short-name-remove-ns:
+        // "{ (u, l) -> { if(l.shortName.lastIndexOf('.') > 0)
+        //     return u.shortName == l.shortName.substring(0, 
l.shortName.lastIndexOf('.'));
+        //     return false; } }"
+        RULE_REGISTRY.put("lower-short-name-remove-ns", new 
Closure<Boolean>(null) {
+            public Boolean doCall(final Service u, final Service l) {
+                int dot = l.getShortName().lastIndexOf('.');
+                if (dot > 0) {
+                    return Objects.equals(
+                        u.getShortName(),
+                        l.getShortName().substring(0, dot));
+                }
+                return false;
+            }
+        });
+
+        // lower-short-name-with-fqdn:
+        // "{ (u, l) -> { if(u.shortName.lastIndexOf(':') > 0)
+        //     return u.shortName.substring(0, u.shortName.lastIndexOf(':'))
+        //         == l.shortName.concat('.svc.cluster.local');
+        //     return false; } }"
+        RULE_REGISTRY.put("lower-short-name-with-fqdn", new 
Closure<Boolean>(null) {
+            public Boolean doCall(final Service u, final Service l) {
+                int colon = u.getShortName().lastIndexOf(':');
+                if (colon > 0) {
+                    return Objects.equals(
+                        u.getShortName().substring(0, colon),
+                        l.getShortName() + ".svc.cluster.local");
+                }
+                return false;
+            }
+        });
+    }
+
+    @Getter
+    private final Map<String, Map<String, MatchingRule>> hierarchyDefinition;
+    @Getter
+    private Map<String, Integer> layerLevels;
+    private Map<String, MatchingRule> matchingRules;
+
+    public HierarchyDefinitionService(CoreModuleConfig moduleConfig) {
+        this.hierarchyDefinition = new HashMap<>();
+        this.layerLevels = new HashMap<>();
+        if (moduleConfig.isEnableHierarchy()) {
+            this.init();
+            this.checkLayers();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void init() {
+        try {
+            Reader applicationReader = 
ResourceUtils.read("hierarchy-definition.yml");
+            Yaml yaml = new Yaml();
+            Map<String, Map> config = yaml.loadAs(applicationReader, 
Map.class);
+            Map<String, Map<String, String>> hierarchy = (Map<String, 
Map<String, String>>) config.get("hierarchy");
+            Map<String, String> matchingRules = (Map<String, String>) 
config.get("auto-matching-rules");
+            this.layerLevels = (Map<String, Integer>) 
config.get("layer-levels");
+            this.matchingRules = matchingRules.entrySet().stream().map(entry 
-> {
+                MatchingRule matchingRule = new MatchingRule(entry.getKey(), 
entry.getValue());
+                return Map.entry(entry.getKey(), matchingRule);
+            }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
+            hierarchy.forEach((layer, lowerLayers) -> {
+                Map<String, MatchingRule> rules = new HashMap<>();
+                lowerLayers.forEach((lowerLayer, ruleName) -> {
+                    rules.put(lowerLayer, this.matchingRules.get(ruleName));
+                });
+                this.hierarchyDefinition.put(layer, rules);
+            });
+        } catch (FileNotFoundException e) {
+            throw new UnexpectedException("hierarchy-definition.yml not 
found.", e);
+        }
+    }
+
+    private void checkLayers() {
+        this.layerLevels.keySet().forEach(layer -> {
+            if (Layer.nameOf(layer).equals(Layer.UNDEFINED)) {
+                throw new IllegalArgumentException(
+                    "hierarchy-definition.yml " + layer + " is not a valid 
layer name.");
+            }
+        });
+        this.hierarchyDefinition.forEach((layer, lowerLayers) -> {
+            Integer layerLevel = this.layerLevels.get(layer);
+            if (this.layerLevels.get(layer) == null) {
+                throw new IllegalArgumentException(
+                    "hierarchy-definition.yml  layer-levels: " + layer + " is 
not defined");
+            }
+
+            for (String lowerLayer : lowerLayers.keySet()) {
+                Integer lowerLayerLevel = this.layerLevels.get(lowerLayer);
+                if (lowerLayerLevel == null) {
+                    throw new IllegalArgumentException(
+                        "hierarchy-definition.yml  layer-levels: " + 
lowerLayer + " is not defined.");
+                }
+                if (layerLevel <= lowerLayerLevel) {
+                    throw new IllegalArgumentException(
+                        "hierarchy-definition.yml hierarchy: " + layer + " 
layer-level should be greater than " + lowerLayer + " layer-level.");
+                }
+            }
+        });
+    }
+
+    @Getter
+    public static class MatchingRule {
+        private final String name;
+        private final String expression;
+        private final Closure<Boolean> closure;
+
+        public MatchingRule(final String name, final String expression) {
+            this.name = name;
+            this.expression = expression;
+            this.closure = RULE_REGISTRY.get(name);
+            if (this.closure == null) {
+                throw new IllegalArgumentException(
+                    "Unknown hierarchy matching rule: " + name
+                        + ". Known rules: " + RULE_REGISTRY.keySet());
+            }
+        }
+    }
+}

Reply via email to