2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END=========================================================
21 package org.onap.policy.m2.base;
23 import java.io.Serializable;
24 import java.time.Instant;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.LinkedList;
28 import java.util.List;
30 import java.util.ServiceLoader;
31 import java.util.UUID;
33 import org.drools.core.WorkingMemory;
34 import org.kie.api.runtime.rule.FactHandle;
35 import org.onap.policy.controlloop.ControlLoopEvent;
36 import org.onap.policy.controlloop.ControlLoopNotification;
37 import org.onap.policy.controlloop.ControlLoopNotificationType;
38 import org.onap.policy.controlloop.ControlLoopOperation;
39 import org.onap.policy.controlloop.policy.ControlLoopPolicy;
40 import org.onap.policy.controlloop.policy.FinalResult;
41 import org.onap.policy.controlloop.policy.Policy;
42 import org.onap.policy.controlloop.policy.PolicyResult;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * Each instance of this class corresonds to a transaction that is in
48 * progress. While active, it resides within Drools memory.
51 public class Transaction implements Serializable {
53 private static Logger logger = LoggerFactory.getLogger(Transaction.class);
55 // This table maps 'actor' names to objects implementing the
56 // 'Actor' interface. 'ServiceLoader' is used to locate and create
57 // these objects, and populate the table.
58 private static Map<String, Actor> nameToActor = new HashMap<>();
61 // use 'ServiceLoader' to locate all of the 'Actor' implementations
63 ServiceLoader.load(Actor.class, Actor.class.getClassLoader())) {
64 logger.debug("Actor: {}, {}", actor.getName(), actor.getClass());
65 nameToActor.put(actor.getName(), actor);
69 private static final long serialVersionUID = 4389211793631707360L;
71 // Drools working memory containing this transaction
73 private transient WorkingMemory workingMemory;
75 // a service-identifier specified on the associated onset message
77 private String closedLoopControlName;
79 // identifies this transaction
81 private UUID requestId;
83 // the decoded YAML file for the policy
84 private ControlLoopPolicy policy;
86 // the initial incoming event
87 private ControlLoopEvent onset = null;
89 // operations specific to the type of 'event'
90 private OnsetAdapter onsetAdapter = null;
92 // the current (or most recent) policy in effect
94 private Policy currentPolicy = null;
96 // the operation currently in progress
98 private Operation currentOperation = null;
100 // a history entry being constructed that is associated with the
101 // currently running operation
102 private ControlLoopOperation histEntry = null;
104 // a list of completed history entries
106 private List<ControlLoopOperation> history = new LinkedList<>();
108 // when the transaction completes, this is the final transaction result
110 private FinalResult finalResult = null;
112 //message, if any, associated with the result of this operation
113 private String message = null;
115 // this table maps a class name into the associated adjunct
116 private Map<Class<?>, Adjunct> adjuncts = new HashMap<>();
119 * Constructor - initialize a 'Transaction' instance
120 * (typically invoked from 'Drools').
122 * @param workingMemory Drools working memory containing this Transaction
123 * @param closedLoopControlName a string identifying the associated service
124 * @param requestId uniquely identifies this transaction
125 * @param policy decoded YAML file containing the policy
128 WorkingMemory workingMemory,
129 String closedLoopControlName,
131 ControlLoopPolicy policy) {
133 logger.info("Transaction constructor");
134 this.workingMemory = workingMemory;
135 this.closedLoopControlName = closedLoopControlName;
136 this.requestId = requestId;
137 this.policy = policy;
141 * Return a string indicating the current state of this transaction.
142 * If there is an operation in progress, the state indicates the operation
143 * state. Otherwise, the state is 'COMPLETE'.
145 * @return a string indicating the current state of this transaction
147 public String getState() {
148 return currentOperation == null
149 ? "COMPLETE" : currentOperation.getState();
153 * Return 'true' if the transaction has completed, and the final result
156 * @return 'true' if the transaction has completed, and the final result
159 public boolean finalResultFailure() {
160 return FinalResult.FINAL_SUCCESS != finalResult
161 && FinalResult.FINAL_OPENLOOP != finalResult
162 && finalResult != null;
166 * Return the overall policy timeout value as a String that can be used
169 * @return the overall policy timeout value as a String that can be used
172 public String getTimeout() {
173 return String.valueOf(policy.getControlLoop().getTimeout()) + "s";
177 * Return the current operation timeout value as a String that can be used
180 * @return the current operation timeout value as a String that can be used
183 public String getOperationTimeout() {
184 return String.valueOf(currentPolicy.getTimeout()) + "s";
188 * Let Drools know the transaction has been modified.
190 * <p>It is not necessary for Java code to call this method when an incoming
191 * message is received for an operation, or an operation timeout occurs --
192 * the Drools code has been written with the assumption that the transaction
193 * is modified in these cases. Instead, this method should be called when
194 * some type of internal occurrence results in a state change, such as when
195 * an operation acquires a lock after initially being blocked.
197 public void modify() {
198 FactHandle handle = workingMemory.getFactHandle(this);
199 if (handle != null) {
200 workingMemory.update(handle, this);
205 * Set the initial 'onset' event that started this transaction.
207 * @param event the initial 'onset' event
209 public void setControlLoopEvent(ControlLoopEvent event) {
211 logger.error("'Transaction' received unexpected event");
217 // fetch associated 'OnsetAdapter'
218 onsetAdapter = OnsetAdapter.get(onset);
220 // check trigger policy type
221 if (isOpenLoop(policy.getControlLoop().getTrigger_policy())) {
222 // no operation is needed for open loops
223 finalResult = FinalResult.FINAL_OPENLOOP;
226 // fetch current policy
227 setPolicyId(policy.getControlLoop().getTrigger_policy());
232 * Validates the onset by ensuring fields that are required
233 * for processing are included in the onset. The fields needed
234 * include the requestId, targetType, and target.
236 * @param onset the initial message that triggers processing
238 public boolean isControlLoopEventValid(ControlLoopEvent onset) {
239 if (onset.getRequestId() == null) {
240 this.message = "No requestID";
242 } else if (onset.getTargetType() == null) {
243 this.message = "No targetType";
245 } else if (onset.getTarget() == null || onset.getTarget().isEmpty()) {
246 this.message = "No target field";
253 * Create a 'ControlLoopNotification' from the specified event. Note thet
254 * the type of the initial 'onset' event is used to determine the type
255 * of the 'ControlLoopNotification', rather than the event passed to the
258 * @param event the event used to generate the notification
259 * (if 'null' is passed, the 'onset' event is used)
260 * @return the created 'ControlLoopNotification' (or subclass) instance
262 public ControlLoopNotification getNotification(ControlLoopEvent event) {
263 ControlLoopNotification notification =
264 onsetAdapter.createNotification(event == null ? this.onset : event);
266 // include entire history
267 notification.setHistory(new ArrayList<>(history));
273 * This method is called when additional incoming messages are received
274 * for the transaction. Messages are routed to the current operation,
275 * any results are processed, and a notification may be returned to
278 * @param object an incoming message, which should be meaningful to the
279 * operation currently in progress
280 * @return a notification message if the operation completed,
281 * or 'null' if it is still in progress
283 public ControlLoopNotification incomingMessage(Object object) {
284 ControlLoopNotification notification = null;
285 if (currentOperation != null) {
286 currentOperation.incomingMessage(object);
287 notification = processResult(currentOperation.getResult());
289 logger.error("'Transaction' received unexpected message: {}", object);
295 * This method is called from Drools when the current operation times out.
297 * @return a notification message if there is an operation in progress,
300 public ControlLoopNotification timeout() {
301 ControlLoopNotification notification = null;
302 if (currentOperation != null) {
303 // notify the current operation
304 currentOperation.timeout();
306 // process the timeout within the transaction
307 notification = processResult(currentOperation.getResult());
309 logger.error("'Transaction' received unexpected timeout");
315 * This method is called from Drools during a control loop timeout
316 * to ensure the correct final notification is sent.
318 public void clTimeout() {
319 this.finalResult = FinalResult.FINAL_FAILURE_TIMEOUT;
320 message = "Control Loop timed out";
321 currentOperation = null;
325 * This method is called from Drools to generate a notification message
326 * when an operation is started.
328 * @return an initial notification message if there is an operation in
329 * progress, or 'null' if not
331 public ControlLoopNotification initialOperationNotification() {
332 if (currentOperation == null || histEntry == null) {
336 ControlLoopNotification notification =
337 onsetAdapter.createNotification(onset);
338 notification.setNotification(ControlLoopNotificationType.OPERATION);
339 notification.setMessage(histEntry.toHistory());
340 notification.setHistory(new LinkedList<>());
341 for (ControlLoopOperation clo : history) {
342 if (histEntry.getOperation().equals(clo.getOperation())
343 && histEntry.getActor().equals(clo.getActor())) {
344 notification.getHistory().add(clo);
351 * Return a final notification message for the entire transaction.
353 * @return a final notification message for the entire transaction,
354 * or 'null' if we don't have a final result yet
356 public ControlLoopNotification finalNotification() {
357 if (finalResult == null) {
361 ControlLoopNotification notification =
362 onsetAdapter.createNotification(onset);
363 switch (finalResult) {
365 notification.setNotification(
366 ControlLoopNotificationType.FINAL_SUCCESS);
369 notification.setNotification(
370 ControlLoopNotificationType.FINAL_OPENLOOP);
373 notification.setNotification(
374 ControlLoopNotificationType.FINAL_FAILURE);
375 notification.setMessage(this.message);
378 notification.setHistory(history);
383 * Return a 'ControlLoopNotification' instance describing the current operation error.
385 * @return a 'ControlLoopNotification' instance describing the current operation error
387 public ControlLoopNotification processError() {
388 ControlLoopNotification notification = null;
389 if (currentOperation != null) {
390 // process the error within the transaction
391 notification = processResult(currentOperation.getResult());
397 * Update the state of the transaction based upon the result of an operation.
399 * @param result if not 'null', this is the result of the current operation
400 * (if 'null', the operation is still in progress,
401 * and no changes are made)
402 * @return if not 'null', this is a notification message that should be
405 private ControlLoopNotification processResult(PolicyResult result) {
406 if (result == null) {
410 String nextPolicy = null;
412 ControlLoopOperation saveHistEntry = histEntry;
413 completeHistEntry(result);
415 final ControlLoopNotification notification = processResultHistEntry(saveHistEntry, result);
417 // If there is a message from the operation then we set it to be
418 // used by the control loop notifications
419 message = currentOperation.getMessage();
421 // set the value 'nextPolicy' based upon the result of the operation
424 nextPolicy = currentPolicy.getSuccess();
428 nextPolicy = processResultFailure();
431 case FAILURE_TIMEOUT:
432 nextPolicy = currentPolicy.getFailure_timeout();
433 message = "Operation timed out";
436 case FAILURE_RETRIES:
437 nextPolicy = currentPolicy.getFailure_retries();
438 message = "Control Loop reached failure retry limit";
441 case FAILURE_EXCEPTION:
442 nextPolicy = currentPolicy.getFailure_exception();
446 nextPolicy = currentPolicy.getFailure_guard();
453 if (nextPolicy != null) {
454 finalResult = FinalResult.toResult(nextPolicy);
455 if (finalResult == null) {
456 // it must be the next state
457 logger.debug("advancing to next operation");
458 setPolicyId(nextPolicy);
460 logger.debug("moving to COMPLETE state");
461 currentOperation = null;
464 logger.debug("doing retry with current actor");
471 // returns a notification message based on the history entry
472 private ControlLoopNotification processResultHistEntry(ControlLoopOperation hist, PolicyResult result) {
477 // generate notification, containing operation history
478 ControlLoopNotification notification = onsetAdapter.createNotification(onset);
479 notification.setNotification(
480 result == PolicyResult.SUCCESS
481 ? ControlLoopNotificationType.OPERATION_SUCCESS
482 : ControlLoopNotificationType.OPERATION_FAILURE);
483 notification.setMessage(hist.toHistory());
485 // include the subset of history that pertains to this
486 // actor and operation
487 notification.setHistory(new LinkedList<>());
488 for (ControlLoopOperation clo : history) {
489 if (hist.getOperation().equals(clo.getOperation())
490 && hist.getActor().equals(clo.getActor())) {
491 notification.getHistory().add(clo);
498 // returns the next policy if the current operation fails
499 private String processResultFailure() {
500 String nextPolicy = null;
501 int attempt = currentOperation.getAttempt();
502 if (attempt <= currentPolicy.getRetry()) {
503 // operation failed, but there are retries left
504 Actor actor = nameToActor.get(currentPolicy.getActor());
507 logger.debug("found Actor, attempt {}", attempt);
509 actor.createOperation(this, currentPolicy, onset, attempt);
512 logger.error("'Transaction' can't find actor {}", currentPolicy.getActor());
515 // operation failed, and no retries (or no retries left)
516 nextPolicy = (attempt == 1
517 ? currentPolicy.getFailure()
518 : currentPolicy.getFailure_retries());
519 logger.debug("moving to policy {}", nextPolicy);
525 * Create a history entry at the beginning of an operation, and store it
526 * in the 'histEntry' instance variable.
528 private void createHistEntry() {
529 histEntry = new ControlLoopOperation();
530 histEntry.setActor(currentPolicy.getActor());
531 histEntry.setOperation(currentPolicy.getRecipe());
532 histEntry.setTarget(currentPolicy.getTarget().toString());
533 histEntry.setSubRequestId(String.valueOf(currentOperation.getAttempt()));
535 // histEntry.end - we will set this one later
536 // histEntry.outcome - we will set this one later
537 // histEntry.message - we will set this one later
541 * Finish up the history entry at the end of an operation, and add it
542 * to the history list.
544 * @param result this is the result of the operation, which can't be 'null'
546 private void completeHistEntry(PolicyResult result) {
547 if (histEntry == null) {
551 // append current entry to history
552 histEntry.setEnd(Instant.now());
553 histEntry.setOutcome(result.toString());
554 histEntry.setMessage(currentOperation.getMessage());
555 history.add(histEntry);
557 // give current operation a chance to act on it
558 currentOperation.histEntryCompleted(histEntry);
559 logger.debug("histEntry = {}", histEntry);
564 * Look up the identifier for the next policy, and prepare to start that
567 * @param id this is the identifier associated with the policy
569 private void setPolicyId(String id) {
570 currentPolicy = null;
571 currentOperation = null;
573 // search through the policies for a matching 'id'
574 for (Policy tmp : policy.getPolicies()) {
575 if (id.equals(tmp.getId())) {
582 if (currentPolicy != null) {
583 // locate the 'Actor' associated with 'currentPolicy'
584 Actor actor = nameToActor.get(currentPolicy.getActor());
586 // found the associated 'Actor' instance
588 actor.createOperation(this, currentPolicy, onset, 1);
591 logger.error("'Transaction' can't find actor {}", currentPolicy.getActor());
594 logger.error("Transaction' can't find policy {}", id);
597 if (currentOperation == null) {
599 // either we couldn't find the actor or the operation --
600 // the transaction fails
601 finalResult = FinalResult.FINAL_FAILURE;
605 private boolean isOpenLoop(String policyId) {
606 return FinalResult.FINAL_OPENLOOP.name().equalsIgnoreCase(policyId);
610 * This method sets the message for a control loop notification
611 * in the case where a custom message wants to be sent due to
612 * error processing, etc.
614 * @param message the message to be set for the control loop notification
616 public void setNotificationMessage(String message) {
617 this.message = message;
621 * Return the notification message of this transaction.
623 * @return the notification message of this transaction
625 public String getNotificationMessage() {
629 /* ============================================================ */
632 * Subclasses of 'Adjunct' provide data and methods to support one or
633 * more Actors/Operations, but are stored within the 'Transaction'
636 public static interface Adjunct extends Serializable {
638 * Called when an adjunct is automatically created as a result of
639 * a 'getAdjunct' call.
641 * @param transaction the transaction containing the adjunct
643 public default void init(Transaction transaction) {
647 * Called for each adjunct when the transaction completes, and is
648 * removed from Drools memory. Any adjunct-specific cleanup can be
649 * done at this point (e.g. freeing locks).
651 public default void cleanup(Transaction transaction) {
656 * This is a method of class 'Transaction', and returns an adjunct of
657 * the specified class (it is created if it doesn't exist).
659 * @param clazz this is the class of the adjunct
660 * @return an adjunct of the specified class ('null' may be returned if
661 * the 'newInstance' method is unable to create the adjunct)
663 public <T extends Adjunct> T getAdjunct(final Class<T> clazz) {
664 return clazz.cast(adjuncts.computeIfAbsent(clazz, cl -> {
667 // create the adjunct (may trigger an exception)
668 adjunct = clazz.getDeclaredConstructor().newInstance();
670 // initialize the adjunct (may also trigger an exception */
671 adjunct.init(Transaction.this);
672 } catch (Exception e) {
673 logger.error("Transaction can't create adjunct of {}", cl, e);
680 * Explicitly create an adjunct -- this is useful when the adjunct
681 * initialization requires that some parameters be passed.
683 * @param adjunct this is the adjunct to insert into the table
684 * @return 'true' if successful
685 * ('false' is returned if an adjunct with this class already exists)
687 public boolean putAdjunct(Adjunct adjunct) {
688 return adjuncts.putIfAbsent(adjunct.getClass(), adjunct) == null;
692 * This method needs to be called when the transaction completes, which
693 * is typically right after it is removed from Drools memory.
695 public void cleanup() {
696 // create a list containing all of the adjuncts (in no particular order)
697 List<Adjunct> values;
698 synchronized (adjuncts) {
699 values = new LinkedList<>(adjuncts.values());
702 // iterate over the list
703 for (Adjunct a : values) {
705 // call the 'cleanup' method on the adjunct
707 } catch (Exception e) {
708 logger.error("Transaction.cleanup exception", e);