You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/04/26 01:40:31 UTC

(skywalking) branch feature/runtime-rule-hot-update updated: log-analyzer: split Factory init into prepare-safe constructor + start-time loadStaticRules

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

wu-sheng pushed a commit to branch feature/runtime-rule-hot-update
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/feature/runtime-rule-hot-update by this push:
     new 98073ffdc5 log-analyzer: split Factory init into prepare-safe constructor + start-time loadStaticRules
98073ffdc5 is described below

commit 98073ffdc5d3609bbdd3ccf0e75b608891e777f4
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sun Apr 26 09:40:23 2026 +0800

    log-analyzer: split Factory init into prepare-safe constructor + start-time loadStaticRules
    
    LogAnalyzerModule.services() declared LogFilterListener.Factory and
    MalConverterRegistry, but the provider was registering both inside
    start() — BootstrapFlow runs requiredCheck BEFORE start(), and the
    count-equals branch of requiredCheck (services.length must match
    services.size) caused ServiceNotProvidedException at boot.
    
    Moving the registration to prepare() needed a small refactor of the
    Factory: its constructor was calling compile() in a loop, which builds
    a RecordSinkListener.Factory that calls moduleManager.find() — illegal
    during the prepare stage (ModuleManager asserts otherwise).
    
    Two-phase init now mirrors the OTLP receiver's own pattern
    (OpenTelemetryMetricRequestProcessor created in prepare(), populated in
    start()):
    
      - Constructor stores moduleManager + config, runs the SPI scan, and
        publishes empty dsls / autoDsls maps. Safe in prepare() — no
        cross-module lookups.
      - New Factory.loadStaticRules() does the LALConfigs.load + per-rule
        compile pass and replaces the empty maps with the populated set.
        Provider calls this from start() once the manager is past prepare.
    
    LogAnalyzerModuleProvider.prepare() now creates and registers the
    Factory + MalConverterRegistry; start() lights up the Factory via
    loadStaticRules and wires it into logAnalyzerService. requiredCheck
    sees both declared services already registered; runtime-rule plugin's
    existing provider().getService(LogFilterListener.Factory.class)
    lookups continue to resolve unchanged.
    
    134 LAL unit tests pass. Verified by booting OAP locally with the
    runtime-rule e2e docker-compose — port 17128 + /runtime/rule/list
    return the static rule snapshot cleanly.
---
 .../v2/provider/LogAnalyzerModuleProvider.java     | 50 ++++++++++++++--------
 .../provider/log/listener/LogFilterListener.java   | 41 +++++++++++++++---
 2 files changed, 69 insertions(+), 22 deletions(-)

diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java
index 1a2ebdf0b1..24f25d1c27 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java
@@ -58,6 +58,7 @@ public class LogAnalyzerModuleProvider extends ModuleProvider {
     private final Object convertersWriteLock = new Object();
 
     private LogAnalyzerServiceImpl logAnalyzerService;
+    private LogFilterListener.Factory factory;
 
     @Override
     public String name() {
@@ -88,26 +89,21 @@ public class LogAnalyzerModuleProvider extends ModuleProvider {
     public void prepare() throws ServiceNotProvidedException, ModuleStartException {
         logAnalyzerService = new LogAnalyzerServiceImpl(getManager(), moduleConfig);
         this.registerServiceImplementation(ILogAnalyzerService.class, logAnalyzerService);
-    }
 
-    @Override
-    public void start() throws ServiceNotProvidedException, ModuleStartException {
-        MeterSystem meterSystem = getManager().find(CoreModule.NAME).provider().getService(MeterSystem.class);
-        for (final var rule : moduleConfig.malConfigs()) {
-            // Use the catalog:name key convention so a runtime-rule /addOrUpdate for the same
-            // (catalog, name) replaces this static entry in place instead of running two
-            // converters against the same sample stream.
-            addOrReplaceMetricConvert("log-mal-rules:" + rule.getName(), new MetricConvert(rule, meterSystem));
-        }
+        // Register both module-declared services in prepare(): {@link BootstrapFlow#start}
+        // runs {@code requiredCheck} against {@code services()} BEFORE this provider's
+        // {@code start()}, and the count-equals check ({@code requiredServices.length ==
+        // services.size()}) requires every declared service to be registered already.
+        // Both objects are config-only at construction — the Factory's heavy
+        // rule-compile pass is deferred to {@link LogFilterListener.Factory#loadStaticRules}
+        // (called from {@link #start()} where moduleManager.find is allowed); the
+        // MalConverterRegistry is a pure delegate to this provider's own volatile map and
+        // never reaches across modules.
         try {
-            final LogFilterListener.Factory factory = new LogFilterListener.Factory(getManager(), moduleConfig);
-            logAnalyzerService.addListenerFactory(factory);
-            // Expose the Factory as a module service so the runtime-rule plugin can compile +
-            // addOrReplace / remove LAL rules at runtime against the same registry the runtime
-            // log pipeline consults.
-            this.registerServiceImplementation(LogFilterListener.Factory.class, factory);
+            this.factory = new LogFilterListener.Factory(getManager(), moduleConfig);
+            this.registerServiceImplementation(LogFilterListener.Factory.class, this.factory);
         } catch (final Exception e) {
-            throw new ModuleStartException("Failed to create LAL listener.", e);
+            throw new ModuleStartException("Failed to create LAL listener factory.", e);
         }
         // MalConverterRegistry for the log-mal-rules catalog. The runtime-rule plugin looks
         // this up by module name + service class, so it does not need a compile-time dep on
@@ -126,6 +122,26 @@ public class LogAnalyzerModuleProvider extends ModuleProvider {
         });
     }
 
+    @Override
+    public void start() throws ServiceNotProvidedException, ModuleStartException {
+        MeterSystem meterSystem = getManager().find(CoreModule.NAME).provider().getService(MeterSystem.class);
+        for (final var rule : moduleConfig.malConfigs()) {
+            // Use the catalog:name key convention so a runtime-rule /addOrUpdate for the same
+            // (catalog, name) replaces this static entry in place instead of running two
+            // converters against the same sample stream.
+            addOrReplaceMetricConvert("log-mal-rules:" + rule.getName(), new MetricConvert(rule, meterSystem));
+        }
+        try {
+            // Light up the Factory now that all peer modules are past prepare:
+            // loadStaticRules calls compile() which constructs RecordSinkListener.Factory
+            // which calls moduleManager.find() — only safe outside prepare.
+            this.factory.loadStaticRules();
+            logAnalyzerService.addListenerFactory(this.factory);
+        } catch (final Exception e) {
+            throw new ModuleStartException("Failed to load static LAL rules.", e);
+        }
+    }
+
     @Override
     public void notifyAfterCompleted() throws ServiceNotProvidedException {
 
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
index 130ed510f3..c3f4af9265 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
@@ -156,13 +156,26 @@ public class LogFilterListener implements LogAnalysisListener {
         private final LogAnalyzerModuleConfig analyzerConfig;
         private final Map<Layer, LALSourceTypeProvider> spiProviders;
 
+        /**
+         * Two-phase init — the constructor wires fields and runs the SPI scan, leaving the
+         * {@code dsls} / {@code autoDsls} registry empty so this instance is safe to register
+         * as a module service from {@code prepare()}. The rule-compile pass — which calls
+         * {@code RecordSinkListener.Factory.<init>} → {@code moduleManager.find()} and is
+         * therefore illegal during the prepare stage — is deferred to
+         * {@link #loadStaticRules()}, which the provider invokes from {@code start()} once
+         * the manager is past prepare.
+         *
+         * <p>This shape mirrors {@code OpenTelemetryMetricRequestProcessor}: the receiver
+         * registers a config-only object in {@code prepare()} and lights it up in
+         * {@code start()}, so cross-module {@code requiredCheck} resolves the service name
+         * cleanly.
+         */
         public Factory(final ModuleManager moduleManager, final LogAnalyzerModuleConfig config) throws Exception {
             this.moduleManager = moduleManager;
             this.analyzerConfig = config;
-            final Map<Layer, Map<String, DSL>> initDsls = new HashMap<>();
-            final Map<String, DSL> initAutoDsls = new HashMap<>();
 
-            // Scan SPI providers for default inputType/outputType per layer
+            // Scan SPI providers for default inputType/outputType per layer. SPI lookup uses
+            // the JDK's {@code ServiceLoader} — no moduleManager.find required, safe in prepare.
             this.spiProviders = new HashMap<>();
             for (final LALSourceTypeProvider p : ServiceLoader.load(LALSourceTypeProvider.class)) {
                 spiProviders.put(p.layer(), p);
@@ -171,7 +184,26 @@ public class LogFilterListener implements LogAnalysisListener {
                     p.outputType() != null ? p.outputType().getName() : "default(Log)");
             }
 
-            final List<LALConfig> configList = LALConfigs.load(config.getLalPath(), config.lalFiles())
+            // Empty registry — populated by {@link #loadStaticRules}.
+            this.dsls = new HashMap<>();
+            this.autoDsls = new HashMap<>();
+        }
+
+        /**
+         * Compile every static LAL rule the {@link LogAnalyzerModuleConfig} configures and
+         * publish the resulting registry. Provider must call this from {@code start()} —
+         * never from {@code prepare()} — because {@code compile} reaches into
+         * {@code RecordSinkListener.Factory.<init>}, which calls {@code moduleManager.find()}
+         * and asserts the manager is past prepare.
+         *
+         * <p>Idempotent against re-entry within the same boot (the Factory only stays in
+         * its empty post-construct state until this fires once); calling it twice on the
+         * same instance is a programming error.
+         */
+        public void loadStaticRules() throws Exception {
+            final Map<Layer, Map<String, DSL>> initDsls = new HashMap<>();
+            final Map<String, DSL> initAutoDsls = new HashMap<>();
+            final List<LALConfig> configList = LALConfigs.load(analyzerConfig.getLalPath(), analyzerConfig.lalFiles())
                                                          .stream()
                                                          .flatMap(it -> it.getRules().stream())
                                                          .collect(Collectors.toList());
@@ -190,7 +222,6 @@ public class LogFilterListener implements LogAnalysisListener {
                     }
                 }
             }
-
             // Publish: readers from now on see the startup-complete registry.
             this.dsls = initDsls;
             this.autoDsls = initAutoDsls;