001/* 002 * Copyright 2025 devteam@scivicslab.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, 011 * software distributed under the License is distributed on an 012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the 014 * specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.scivicslab.actoriac; 019 020import java.util.List; 021import java.util.concurrent.ExecutorService; 022 023import org.json.JSONObject; 024 025import com.scivicslab.actoriac.log.DistributedLogStore; 026import com.scivicslab.actoriac.log.LogLevel; 027import com.scivicslab.pojoactor.core.ActionResult; 028import com.scivicslab.pojoactor.core.ActorRef; 029import com.scivicslab.pojoactor.workflow.IIActorRef; 030import com.scivicslab.pojoactor.workflow.IIActorSystem; 031import com.scivicslab.pojoactor.workflow.Interpreter; 032import com.scivicslab.pojoactor.workflow.Transition; 033 034/** 035 * Level 3 wrapper that adds workflow capabilities to a NodeGroup POJO. 036 * 037 * <p>This class extends {@link Interpreter} to provide workflow execution 038 * capabilities while delegating node group operations to a wrapped {@link NodeGroup} instance.</p> 039 * 040 * <p>This follows the same three-level architecture as NodeInterpreter:</p> 041 * <ul> 042 * <li><strong>Level 1 (POJO):</strong> {@link NodeGroup} - pure POJO with inventory management</li> 043 * <li><strong>Level 2 (Actor):</strong> ActorRef<NodeGroup> - actor wrapper for concurrent execution</li> 044 * <li><strong>Level 3 (Workflow):</strong> NodeGroupInterpreter - workflow capabilities + IIActorRef wrapper</li> 045 * </ul> 046 * 047 * <p><strong>Design principle:</strong> NodeGroup remains a pure POJO, independent of ActorSystem. 048 * NodeGroupInterpreter wraps NodeGroup to add workflow capabilities without modifying the NodeGroup class.</p> 049 * 050 * @author devteam@scivicslab.com 051 */ 052public class NodeGroupInterpreter extends Interpreter { 053 054 /** 055 * The wrapped NodeGroup POJO that handles inventory and node creation. 056 */ 057 private final NodeGroup nodeGroup; 058 059 /** 060 * The overlay directory path for YAML overlay feature. 061 * When set, workflows are loaded with overlay applied. 062 */ 063 private String overlayDir; 064 065 /** 066 * Verbose output flag. 067 * When true, displays full YAML for each transition instead of truncated version. 068 */ 069 private boolean verbose = false; 070 071 /** 072 * IaCStreamingAccumulator for displaying workflow steps with cowsay ASCII art. 073 * When set, workflow step transitions are displayed using this accumulator. 074 */ 075 private IaCStreamingAccumulator accumulator = null; 076 077 /** 078 * Actor reference for the distributed log store. 079 * Used to send log messages asynchronously to avoid blocking workflow execution. 080 */ 081 private ActorRef<DistributedLogStore> logStoreActor; 082 083 /** 084 * Direct reference to the log store for synchronous read operations. 085 * Reads don't need to go through the actor since they don't need serialization. 086 */ 087 private DistributedLogStore logStore; 088 089 /** 090 * Dedicated executor service for DB writes. 091 * Should be a single-threaded pool to ensure serialized writes. 092 */ 093 private ExecutorService dbExecutor; 094 095 /** 096 * Session ID for the current workflow execution. 097 */ 098 private long sessionId = -1; 099 100 /** 101 * Constructs a NodeGroupInterpreter that wraps the specified NodeGroup. 102 * 103 * @param nodeGroup the {@link NodeGroup} instance to wrap 104 * @param system the actor system for workflow execution 105 */ 106 public NodeGroupInterpreter(NodeGroup nodeGroup, IIActorSystem system) { 107 super(); 108 this.nodeGroup = nodeGroup; 109 this.system = system; 110 } 111 112 /** 113 * Creates Node objects for all hosts in the specified group. 114 * 115 * <p>Delegates to the wrapped {@link NodeGroup#createNodesForGroup(String)} method.</p> 116 * 117 * @param groupName the name of the group from the inventory file 118 * @return the list of created Node objects 119 */ 120 public List<Node> createNodesForGroup(String groupName) { 121 return nodeGroup.createNodesForGroup(groupName); 122 } 123 124 /** 125 * Creates a single Node for localhost execution. 126 * 127 * <p>Delegates to the wrapped {@link NodeGroup#createLocalNode()} method.</p> 128 * 129 * @return a list containing a single localhost Node 130 */ 131 public List<Node> createLocalNode() { 132 return nodeGroup.createLocalNode(); 133 } 134 135 /** 136 * Gets the inventory object. 137 * 138 * @return the loaded inventory, or null if not loaded 139 */ 140 public InventoryParser.Inventory getInventory() { 141 return nodeGroup.getInventory(); 142 } 143 144 /** 145 * Gets the wrapped NodeGroup instance. 146 * 147 * <p>This allows direct access to the underlying POJO when needed.</p> 148 * 149 * @return the wrapped NodeGroup 150 */ 151 public NodeGroup getNodeGroup() { 152 return nodeGroup; 153 } 154 155 /** 156 * Sets the overlay directory for YAML overlay feature. 157 * 158 * <p>When an overlay directory is set, workflows will be loaded with 159 * overlay applied, allowing environment-specific configuration.</p> 160 * 161 * @param overlayDir the path to the overlay directory containing overlay-conf.yaml 162 */ 163 public void setOverlayDir(String overlayDir) { 164 this.overlayDir = overlayDir; 165 } 166 167 /** 168 * Gets the overlay directory path. 169 * 170 * @return the overlay directory path, or null if not set 171 */ 172 public String getOverlayDir() { 173 return overlayDir; 174 } 175 176 /** 177 * Sets verbose mode for detailed output. 178 * 179 * <p>When enabled, displays full YAML for each transition in cowsay output 180 * instead of the truncated version.</p> 181 * 182 * @param verbose true to enable verbose output 183 */ 184 public void setVerbose(boolean verbose) { 185 this.verbose = verbose; 186 } 187 188 /** 189 * Checks if verbose mode is enabled. 190 * 191 * @return true if verbose mode is enabled 192 */ 193 public boolean isVerbose() { 194 return verbose; 195 } 196 197 /** 198 * Sets the IaCStreamingAccumulator for displaying workflow steps. 199 * 200 * <p>When set, workflow step transitions are displayed using cowsay ASCII art 201 * via this accumulator. The accumulator's cowfile setting determines which 202 * character is used.</p> 203 * 204 * @param accumulator the accumulator to use for cowsay display 205 */ 206 public void setAccumulator(IaCStreamingAccumulator accumulator) { 207 this.accumulator = accumulator; 208 } 209 210 /** 211 * Gets the IaCStreamingAccumulator for displaying workflow steps. 212 * 213 * @return the cowsay accumulator, or null if not set 214 */ 215 public IaCStreamingAccumulator getAccumulator() { 216 return accumulator; 217 } 218 219 /** 220 * Sets the distributed log store for structured logging. 221 * 222 * <p>Database writes are performed asynchronously via the logStore actor 223 * using the dedicated dbExecutor to avoid blocking workflow execution. 224 * Direct reads can use the logStore reference directly.</p> 225 * 226 * @param logStore the log store instance (for direct reads) 227 * @param logStoreActor the actor reference for the log store (for async writes) 228 * @param dbExecutor the dedicated executor service for DB writes 229 * @param sessionId the session ID for this execution 230 */ 231 public void setLogStore(DistributedLogStore logStore, 232 ActorRef<DistributedLogStore> logStoreActor, 233 ExecutorService dbExecutor, long sessionId) { 234 this.logStore = logStore; 235 this.logStoreActor = logStoreActor; 236 this.dbExecutor = dbExecutor; 237 this.sessionId = sessionId; 238 } 239 240 /** 241 * Gets the log store for direct read operations. 242 * 243 * @return the log store, or null if not set 244 */ 245 public DistributedLogStore getLogStore() { 246 return logStore; 247 } 248 249 /** 250 * Gets the log store actor for async write operations. 251 * 252 * @return the log store actor, or null if not set 253 */ 254 public ActorRef<DistributedLogStore> getLogStoreActor() { 255 return logStoreActor; 256 } 257 258 /** 259 * Gets the DB executor service. 260 * 261 * @return the DB executor service, or null if not set 262 */ 263 public ExecutorService getDbExecutor() { 264 return dbExecutor; 265 } 266 267 /** 268 * Gets the session ID. 269 * 270 * @return the session ID, or -1 if not set 271 */ 272 public long getSessionId() { 273 return sessionId; 274 } 275 276 /** 277 * Hook called when entering a transition during workflow execution. 278 * 279 * <p>Displays the workflow name and transition definition using cowsay. 280 * In normal mode, shows first 10 lines. In verbose mode, shows the full YAML 281 * after the cowsay output.</p> 282 * 283 * @param transition the transition being entered 284 */ 285 @Override 286 protected void onEnterTransition(Transition transition) { 287 // Get workflow name 288 String workflowName = (getCode() != null && getCode().getName() != null) 289 ? getCode().getName() 290 : "unknown-workflow"; 291 292 // Get YAML-formatted output (first 10 lines for cowsay) 293 String yamlText = transition.toYamlString(10).trim(); 294 295 // Render cowsay output 296 String cowsayOutput; 297 if (accumulator != null) { 298 cowsayOutput = accumulator.renderCowsay(workflowName, yamlText); 299 } else { 300 // Fallback to simple text if no accumulator 301 cowsayOutput = "[" + workflowName + "]\n" + yamlText; 302 } 303 304 // Send cowsay output to outputMultiplexer (loose coupling via ActorSystem) 305 IIActorRef<?> multiplexer = system.getIIActor("outputMultiplexer"); 306 // Use actor name as source (consistent across all output) 307 String actorName = selfActorRef != null ? selfActorRef.getName() : "unknown"; 308 if (multiplexer != null) { 309 JSONObject arg = new JSONObject(); 310 arg.put("source", actorName); 311 arg.put("type", "cowsay"); 312 arg.put("data", cowsayOutput); 313 multiplexer.callByActionName("add", arg.toString()); 314 } 315 316 // In verbose mode, also send the full YAML 317 if (verbose) { 318 String fullYaml = transition.toYamlString(-1); 319 String verboseOutput = "--- Full transition YAML ---\n" + fullYaml + "\n----------------------------"; 320 if (multiplexer != null) { 321 JSONObject arg = new JSONObject(); 322 arg.put("source", actorName); 323 arg.put("type", "verbose"); 324 arg.put("data", verboseOutput); 325 multiplexer.callByActionName("add", arg.toString()); 326 } 327 } 328 329 // Log to distributed log store asynchronously 330 if (logStoreActor != null && sessionId >= 0) { 331 String label = transition.getLabel(); 332 if (label == null && transition.getStates() != null && transition.getStates().size() >= 2) { 333 label = transition.getStates().get(0) + " -> " + transition.getStates().get(1); 334 } 335 final String finalLabel = label; 336 final String message = "Entering transition: " + yamlText.split("\n")[0]; 337 // Fire-and-forget: don't wait for DB write to complete 338 logStoreActor.tell( 339 store -> store.log(sessionId, "nodeGroup", finalLabel, LogLevel.INFO, message), 340 dbExecutor); 341 } 342 } 343 344 /** 345 * Hook called after a transition completes (success or failure). 346 * 347 * <p>Logs the transition result to the distributed log store for 348 * workflow execution reporting.</p> 349 * 350 * @param transition the transition that was attempted 351 * @param success true if the transition succeeded, false if it failed 352 * @param result the ActionResult from executing the transition's actions 353 */ 354 @Override 355 protected void onExitTransition(Transition transition, boolean success, ActionResult result) { 356 if (logStoreActor == null || sessionId < 0) { 357 return; 358 } 359 360 String label = transition.getLabel(); 361 if (label == null && transition.getStates() != null && transition.getStates().size() >= 2) { 362 label = transition.getStates().get(0) + " -> " + transition.getStates().get(1); 363 } 364 final String finalLabel = label; 365 366 // noteを取得(60文字または最初の行まで) 367 String note = getTransitionNote(transition); 368 369 String status = success ? "SUCCESS" : "FAILED"; 370 String resultMsg = result != null ? result.getResult() : ""; 371 // Truncate long result messages 372 if (resultMsg.length() > 500) { 373 resultMsg = resultMsg.substring(0, 500) + "..."; 374 } 375 // noteがあればメッセージに含める 376 final String message = "Transition " + status + ": " + finalLabel + 377 (note.isEmpty() ? "" : " [" + note + "]") + 378 (resultMsg.isEmpty() ? "" : " - " + resultMsg); 379 380 LogLevel level = success ? LogLevel.INFO : LogLevel.WARN; 381 382 // Fire-and-forget: don't wait for DB write to complete 383 logStoreActor.tell( 384 store -> store.log(sessionId, "nodeGroup", finalLabel, level, message), 385 dbExecutor); 386 } 387 388 /** 389 * transitionのnoteを取得する。60文字または最初の行までに制限。 390 * noteがない場合は空文字列を返す。 391 */ 392 private String getTransitionNote(Transition transition) { 393 String note = transition.getNote(); 394 if (note == null || note.isEmpty()) { 395 return ""; 396 } 397 // 最初の行のみ 398 int newlineIdx = note.indexOf('\n'); 399 if (newlineIdx > 0) { 400 note = note.substring(0, newlineIdx); 401 } 402 // 60文字まで 403 if (note.length() > 60) { 404 note = note.substring(0, 57) + "..."; 405 } 406 return note.trim(); 407 } 408}