jsinovassin commented on code in PR #720:
URL: https://github.com/apache/unomi/pull/720#discussion_r2216021184


##########
extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/services/impl/GroovyActionsServiceImpl.java:
##########
@@ -209,48 +328,182 @@ private void saveActionType(Action action) {
         definitionsService.setActionType(actionType);
     }
 
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public void remove(String id) {
-        if (groovyCodeSourceMap.containsKey(id)) {
-            try {
-                definitionsService.removeActionType(
-                        
groovyShell.parse(groovyCodeSourceMap.get(id)).getClass().getMethod("execute").getAnnotation(Action.class).id());
-            } catch (NoSuchMethodException e) {
-                LOGGER.error("Failed to delete the action type for the id {}", 
id, e);
+        validateNotEmpty(id, "Script ID");
+
+        LOGGER.info("Removing script: {}", id);
+
+        ScriptMetadata removedMetadata = scriptMetadataCache.remove(id);
+        persistenceService.remove(id, GroovyAction.class);
+
+        if (removedMetadata != null) {
+            Action actionAnnotation = 
getActionAnnotation(removedMetadata.getCompiledClass());
+            if (actionAnnotation != null) {
+                definitionsService.removeActionType(actionAnnotation.id());
             }
-            persistenceService.remove(id, GroovyAction.class);
         }
+
+        LOGGER.info("Script {} removed successfully", id);
     }
 
+
+    /**
+     * {@inheritDoc}
+     * Performance Warning: Compiles on-demand if not cached.
+     */
     @Override
-    public GroovyCodeSource getGroovyCodeSource(String id) {
-        return groovyCodeSourceMap.get(id);
+    public Class<? extends Script> getOrCompileScript(String id) {
+        validateNotEmpty(id, "Script ID");
+
+        ScriptMetadata metadata = scriptMetadataCache.get(id);
+        if (metadata != null) {
+            return metadata.getCompiledClass();
+        }
+
+        long startTime = System.currentTimeMillis();
+        LOGGER.warn("Script {} not found in cache, compiling on-demand 
(performance warning)", id);
+
+        GroovyAction groovyAction = persistenceService.load(id, 
GroovyAction.class);
+        if (groovyAction == null) {
+            LOGGER.warn("Script {} not found in persistence, returning null 
({}ms)", id,
+                System.currentTimeMillis() - startTime);
+            return null;
+        }
+
+        try {
+            long compilationStartTime = System.currentTimeMillis();
+            ScriptMetadata newMetadata = compileAndCreateMetadata(id, 
groovyAction.getScript());
+            long compilationTime = System.currentTimeMillis() - 
compilationStartTime;
+
+            scriptMetadataCache.put(id, newMetadata);
+
+            long totalTime = System.currentTimeMillis() - startTime;
+            LOGGER.warn("On-demand compilation completed for {} (total: {}ms, 
compilation: {}ms)",
+                id, totalTime, compilationTime);
+            return newMetadata.getCompiledClass();
+        } catch (Exception e) {
+            long totalTime = System.currentTimeMillis() - startTime;
+            LOGGER.error("Failed to compile script {} on-demand ({}ms)", id, 
totalTime, e);
+            return null;
+        }
     }
 
     /**
-     * Build a GroovyCodeSource object and add it to the class loader of the 
groovyScriptEngine
-     *
-     * @param groovyScript groovy script as a string
-     * @param actionName   Name of the action
-     * @return Built GroovyCodeSource
+     * {@inheritDoc}
+     */
+    @Override
+    public Class<? extends Script> getCompiledScript(String id) {
+        validateNotEmpty(id, "Script ID");
+
+        ScriptMetadata metadata = scriptMetadataCache.get(id);
+        if (metadata == null) {
+            LOGGER.warn("Script {} not found in cache", id);
+            return null;
+        }
+        return metadata.getCompiledClass();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ScriptMetadata getScriptMetadata(String actionName) {
+        validateNotEmpty(actionName, "Action name");
+
+        return scriptMetadataCache.get(actionName);
+    }
+
+    /**
+     * Creates GroovyCodeSource for compilation.
      */
     private GroovyCodeSource buildClassScript(String groovyScript, String 
actionName) {
         return new GroovyCodeSource(groovyScript, actionName, 
"/groovy/script");
     }
 
+    /**
+     * Persists script to storage.
+     */
     private void saveScript(String actionName, String script) {
         GroovyAction groovyScript = new GroovyAction(actionName, script);
         persistenceService.save(groovyScript);
         LOGGER.info("The script {} has been persisted.", actionName);
     }
 
+    /**
+     * Refreshes scripts from persistence with selective recompilation.
+     * Uses hash-based change detection and atomic cache updates.
+     */
     private void refreshGroovyActions() {
-        Map<String, GroovyCodeSource> refreshedGroovyCodeSourceMap = new 
HashMap<>();
-        
persistenceService.getAllItems(GroovyAction.class).forEach(groovyAction -> 
refreshedGroovyCodeSourceMap
-                .put(groovyAction.getName(), 
buildClassScript(groovyAction.getScript(), groovyAction.getName())));
-        groovyCodeSourceMap = refreshedGroovyCodeSourceMap;
+        long startTime = System.currentTimeMillis();
+
+        Map<String, ScriptMetadata> newMetadataCache = new 
ConcurrentHashMap<>();
+        int unchangedCount = 0;
+        int recompiledCount = 0;
+        int errorCount = 0;
+        long totalCompilationTime = 0;
+
+        for (GroovyAction groovyAction : 
persistenceService.getAllItems(GroovyAction.class)) {
+            String actionName = groovyAction.getName();
+            String scriptContent = groovyAction.getScript();
+
+            try {
+                ScriptMetadata existingMetadata = 
scriptMetadataCache.get(actionName);
+                if (existingMetadata != null && 
!existingMetadata.hasChanged(scriptContent)) {
+                    newMetadataCache.put(actionName, existingMetadata);
+                    unchangedCount++;
+                    LOGGER.debug("Script {} unchanged during refresh, keeping 
cached version", actionName);
+                } else {
+                    if (recompiledCount == 0) {
+                        LOGGER.info("Refreshing scripts from persistence 
layer...");
+                    }
+
+                    long compilationStartTime = System.currentTimeMillis();
+                    ScriptMetadata metadata = 
compileAndCreateMetadata(actionName, scriptContent);
+                    long compilationTime = System.currentTimeMillis() - 
compilationStartTime;
+                    totalCompilationTime += compilationTime;
+
+                    newMetadataCache.put(actionName, metadata);
+                    recompiledCount++;
+                    LOGGER.info("Script {} recompiled during refresh ({}ms)", 
actionName, compilationTime);
+                }
+
+            } catch (Exception e) {
+                if (errorCount == 0 && recompiledCount == 0) {
+                    LOGGER.info("Refreshing scripts from persistence 
layer...");
+                }
+
+                errorCount++;
+                LOGGER.error("Failed to refresh script: {}", actionName, e);
+
+                ScriptMetadata existingMetadata = 
scriptMetadataCache.get(actionName);
+                if (existingMetadata != null) {
+                    newMetadataCache.put(actionName, existingMetadata);
+                    LOGGER.warn("Keeping existing version of script {} due to 
compilation error", actionName);
+                }
+            }
+        }
+
+        this.scriptMetadataCache = newMetadataCache;
+
+        if (recompiledCount > 0 || errorCount > 0) {
+            long totalTime = System.currentTimeMillis() - startTime;
+            LOGGER.info("Script refresh completed: {} unchanged, {} 
recompiled, {} errors. Total time: {}ms",

Review Comment:
   By default the refresh is done each 1 seconde. If one of the script is in 
error, the log will be displayed each seconds. It can lead to log pollution.
   Maybe we can separate the case where recompiledCount > 0 and errorCount > 0 
as an error will already be displayed in the catch above



-- 
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: dev-unsubscr...@unomi.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to