Merge "remove nexus function from entrypoint"
[policy/drools-applications.git] / controlloop / m2 / base / src / main / java / org / onap / policy / m2 / base / Transaction.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * m2/base
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
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
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=========================================================
19  */
20
21 package org.onap.policy.m2.base;
22
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;
29 import java.util.Map;
30 import java.util.ServiceLoader;
31 import java.util.UUID;
32 import lombok.Getter;
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;
45
46 /*
47  * Each instance of this class corresonds to a transaction that is in
48  * progress. While active, it resides within Drools memory.
49  */
50
51 public class Transaction implements Serializable {
52
53     private static Logger logger = LoggerFactory.getLogger(Transaction.class);
54
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<>();
59
60     static {
61         // use 'ServiceLoader' to locate all of the 'Actor' implementations
62         for (Actor actor :
63                 ServiceLoader.load(Actor.class, Actor.class.getClassLoader())) {
64             logger.debug("Actor: {}, {}", actor.getName(), actor.getClass());
65             nameToActor.put(actor.getName(), actor);
66         }
67     }
68
69     private static final long serialVersionUID = 4389211793631707360L;
70
71     // Drools working memory containing this transaction
72     @Getter
73     private transient WorkingMemory workingMemory;
74
75     // a service-identifier specified on the associated onset message
76     @Getter
77     private String closedLoopControlName;
78
79     // identifies this transaction
80     @Getter
81     private UUID requestId;
82
83     // the decoded YAML file for the policy
84     private ControlLoopPolicy policy;
85
86     // the initial incoming event
87     private ControlLoopEvent onset = null;
88
89     // operations specific to the type of 'event'
90     private OnsetAdapter onsetAdapter = null;
91
92     // the current (or most recent) policy in effect
93     @Getter
94     private Policy currentPolicy = null;
95
96     // the operation currently in progress
97     @Getter
98     private Operation currentOperation = null;
99
100     // a history entry being constructed that is associated with the
101     // currently running operation
102     private ControlLoopOperation histEntry = null;
103
104     // a list of completed history entries
105     @Getter
106     private List<ControlLoopOperation> history = new LinkedList<>();
107
108     // when the transaction completes, this is the final transaction result
109     @Getter
110     private FinalResult finalResult = null;
111
112     //message, if any, associated with the result of this operation
113     private String message = null;
114
115     // this table maps a class name into the associated adjunct
116     private Map<Class<?>, Adjunct> adjuncts = new HashMap<>();
117
118     /**
119      * Constructor - initialize a 'Transaction' instance
120      * (typically invoked from 'Drools').
121      *
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
126      */
127     public Transaction(
128         WorkingMemory workingMemory,
129         String closedLoopControlName,
130         UUID requestId,
131         ControlLoopPolicy policy) {
132
133         logger.info("Transaction constructor");
134         this.workingMemory = workingMemory;
135         this.closedLoopControlName = closedLoopControlName;
136         this.requestId = requestId;
137         this.policy = policy;
138     }
139
140     /**
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'.
144      *
145      * @return a string indicating the current state of this transaction
146      */
147     public String getState() {
148         return currentOperation == null
149             ? "COMPLETE" : currentOperation.getState();
150     }
151
152     /**
153      * Return 'true' if the transaction has completed, and the final result
154      * indicates failure.
155      *
156      * @return 'true' if the transaction has completed, and the final result
157      *     indicates failure
158      */
159     public boolean finalResultFailure() {
160         return FinalResult.FINAL_SUCCESS != finalResult
161                && FinalResult.FINAL_OPENLOOP != finalResult
162                && finalResult != null;
163     }
164
165     /**
166      * Return the overall policy timeout value as a String that can be used
167      * in a Drools timer.
168      *
169      * @return the overall policy timeout value as a String that can be used
170      *     in a Drools timer
171      */
172     public String getTimeout() {
173         return String.valueOf(policy.getControlLoop().getTimeout()) + "s";
174     }
175
176     /**
177      * Return the current operation timeout value as a String that can be used
178      * in a Drools timer.
179      *
180      * @return the current operation timeout value as a String that can be used
181      *     in a Drools timer
182      */
183     public String getOperationTimeout() {
184         return String.valueOf(currentPolicy.getTimeout()) + "s";
185     }
186
187     /**
188      * Let Drools know the transaction has been modified.
189      *
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.
196      */
197     public void modify() {
198         FactHandle handle = workingMemory.getFactHandle(this);
199         if (handle != null) {
200             workingMemory.update(handle, this);
201         }
202     }
203
204     /**
205      * Set the initial 'onset' event that started this transaction.
206      *
207      * @param event the initial 'onset' event
208      */
209     public void setControlLoopEvent(ControlLoopEvent event) {
210         if (onset != null) {
211             logger.error("'Transaction' received unexpected event");
212             return;
213         }
214
215         onset = event;
216
217         // fetch associated 'OnsetAdapter'
218         onsetAdapter = OnsetAdapter.get(onset);
219
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;
224             modify();
225         } else {
226             // fetch current policy
227             setPolicyId(policy.getControlLoop().getTrigger_policy());
228         }
229     }
230
231     /**
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.
235      *
236      * @param onset the initial message that triggers processing
237      */
238     public boolean isControlLoopEventValid(ControlLoopEvent onset) {
239         if (onset.getRequestId() == null) {
240             this.message = "No requestID";
241             return false;
242         } else if (onset.getTargetType() == null) {
243             this.message = "No targetType";
244             return false;
245         } else if (onset.getTarget() == null || onset.getTarget().isEmpty()) {
246             this.message = "No target field";
247             return false;
248         }
249         return true;
250     }
251
252     /**
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
256      * method.
257      *
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
261      */
262     public ControlLoopNotification getNotification(ControlLoopEvent event) {
263         ControlLoopNotification notification =
264             onsetAdapter.createNotification(event == null ? this.onset : event);
265
266         // include entire history
267         notification.setHistory(new ArrayList<>(history));
268
269         return notification;
270     }
271
272     /**
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
276      * the caller.
277      *
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
282      */
283     public ControlLoopNotification incomingMessage(Object object) {
284         ControlLoopNotification notification = null;
285         if (currentOperation != null) {
286             currentOperation.incomingMessage(object);
287             notification = processResult(currentOperation.getResult());
288         } else {
289             logger.error("'Transaction' received unexpected message: {}", object);
290         }
291         return notification;
292     }
293
294     /**
295      * This method is called from Drools when the current operation times out.
296      *
297      * @return a notification message if there is an operation in progress,
298      *     or 'null' if not
299      */
300     public ControlLoopNotification timeout() {
301         ControlLoopNotification notification = null;
302         if (currentOperation != null) {
303             // notify the current operation
304             currentOperation.timeout();
305
306             // process the timeout within the transaction
307             notification = processResult(currentOperation.getResult());
308         } else {
309             logger.error("'Transaction' received unexpected timeout");
310         }
311         return notification;
312     }
313
314     /**
315      *  This method is called from Drools during a control loop timeout
316      *  to ensure the correct final notification is sent.
317      */
318     public void clTimeout() {
319         this.finalResult = FinalResult.FINAL_FAILURE_TIMEOUT;
320         message = "Control Loop timed out";
321         currentOperation = null;
322     }
323
324     /**
325      * This method is called from Drools to generate a notification message
326      * when an operation is started.
327      *
328      * @return an initial notification message if there is an operation in
329      *     progress, or 'null' if not
330      */
331     public ControlLoopNotification initialOperationNotification() {
332         if (currentOperation == null || histEntry == null) {
333             return null;
334         }
335
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);
345             }
346         }
347         return notification;
348     }
349
350     /**
351      * Return a final notification message for the entire transaction.
352      *
353      * @return a final notification message for the entire transaction,
354      *     or 'null' if we don't have a final result yet
355      */
356     public ControlLoopNotification finalNotification() {
357         if (finalResult == null) {
358             return null;
359         }
360
361         ControlLoopNotification notification =
362             onsetAdapter.createNotification(onset);
363         switch (finalResult) {
364             case FINAL_SUCCESS:
365                 notification.setNotification(
366                     ControlLoopNotificationType.FINAL_SUCCESS);
367                 break;
368             case FINAL_OPENLOOP:
369                 notification.setNotification(
370                     ControlLoopNotificationType.FINAL_OPENLOOP);
371                 break;
372             default:
373                 notification.setNotification(
374                     ControlLoopNotificationType.FINAL_FAILURE);
375                 notification.setMessage(this.message);
376                 break;
377         }
378         notification.setHistory(history);
379         return notification;
380     }
381
382     /**
383      * Return a 'ControlLoopNotification' instance describing the current operation error.
384      *
385      * @return a 'ControlLoopNotification' instance describing the current operation error
386      */
387     public ControlLoopNotification processError() {
388         ControlLoopNotification notification = null;
389         if (currentOperation != null) {
390             // process the error within the transaction
391             notification = processResult(currentOperation.getResult());
392         }
393         return notification;
394     }
395
396     /**
397      * Update the state of the transaction based upon the result of an operation.
398      *
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
403      *     sent to RUBY
404      */
405     private ControlLoopNotification processResult(PolicyResult result) {
406         if (result == null) {
407             modify();
408             return null;
409         }
410         String nextPolicy = null;
411
412         ControlLoopOperation saveHistEntry = histEntry;
413         completeHistEntry(result);
414
415         final ControlLoopNotification notification = processResultHistEntry(saveHistEntry, result);
416
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();
420
421         // set the value 'nextPolicy' based upon the result of the operation
422         switch (result) {
423             case SUCCESS:
424                 nextPolicy = currentPolicy.getSuccess();
425                 break;
426
427             case FAILURE:
428                 nextPolicy = processResultFailure();
429                 break;
430
431             case FAILURE_TIMEOUT:
432                 nextPolicy = currentPolicy.getFailure_timeout();
433                 message = "Operation timed out";
434                 break;
435
436             case FAILURE_RETRIES:
437                 nextPolicy = currentPolicy.getFailure_retries();
438                 message = "Control Loop reached failure retry limit";
439                 break;
440
441             case FAILURE_EXCEPTION:
442                 nextPolicy = currentPolicy.getFailure_exception();
443                 break;
444
445             case FAILURE_GUARD:
446                 nextPolicy = currentPolicy.getFailure_guard();
447                 break;
448
449             default:
450                 break;
451         }
452
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);
459             } else {
460                 logger.debug("moving to COMPLETE state");
461                 currentOperation = null;
462             }
463         } else {
464             logger.debug("doing retry with current actor");
465         }
466
467         modify();
468         return notification;
469     }
470
471     // returns a notification message based on the history entry
472     private ControlLoopNotification processResultHistEntry(ControlLoopOperation hist, PolicyResult result) {
473         if (hist == null) {
474             return null;
475         }
476
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());
484
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);
492             }
493         }
494
495         return notification;
496     }
497
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());
505             if (actor != null) {
506                 attempt += 1;
507                 logger.debug("found Actor, attempt {}", attempt);
508                 currentOperation =
509                     actor.createOperation(this, currentPolicy, onset, attempt);
510                 createHistEntry();
511             } else {
512                 logger.error("'Transaction' can't find actor {}", currentPolicy.getActor());
513             }
514         } else {
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);
520         }
521         return nextPolicy;
522     }
523
524     /**
525      * Create a history entry at the beginning of an operation, and store it
526      * in the 'histEntry' instance variable.
527      */
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()));
534
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
538     }
539
540     /**
541      * Finish up the history entry at the end of an operation, and add it
542      * to the history list.
543      *
544      * @param result this is the result of the operation, which can't be 'null'
545      */
546     private void completeHistEntry(PolicyResult result) {
547         if (histEntry == null) {
548             return;
549         }
550
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);
556
557         // give current operation a chance to act on it
558         currentOperation.histEntryCompleted(histEntry);
559         logger.debug("histEntry = {}", histEntry);
560         histEntry = null;
561     }
562
563     /**
564      * Look up the identifier for the next policy, and prepare to start that
565      * operation.
566      *
567      * @param id this is the identifier associated with the policy
568      */
569     private void setPolicyId(String id) {
570         currentPolicy = null;
571         currentOperation = null;
572
573         // search through the policies for a matching 'id'
574         for (Policy tmp : policy.getPolicies()) {
575             if (id.equals(tmp.getId())) {
576                 // found a match
577                 currentPolicy = tmp;
578                 break;
579             }
580         }
581
582         if (currentPolicy != null) {
583             // locate the 'Actor' associated with 'currentPolicy'
584             Actor actor = nameToActor.get(currentPolicy.getActor());
585             if (actor != null) {
586                 // found the associated 'Actor' instance
587                 currentOperation =
588                     actor.createOperation(this, currentPolicy, onset, 1);
589                 createHistEntry();
590             } else {
591                 logger.error("'Transaction' can't find actor {}", currentPolicy.getActor());
592             }
593         } else {
594             logger.error("Transaction' can't find policy {}", id);
595         }
596
597         if (currentOperation == null) {
598
599             // either we couldn't find the actor or the operation --
600             // the transaction fails
601             finalResult = FinalResult.FINAL_FAILURE;
602         }
603     }
604
605     private boolean isOpenLoop(String policyId) {
606         return FinalResult.FINAL_OPENLOOP.name().equalsIgnoreCase(policyId);
607     }
608
609     /**
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.
613      *
614      * @param message the message to be set for the control loop notification
615      */
616     public void setNotificationMessage(String message) {
617         this.message = message;
618     }
619
620     /**
621      * Return the notification message of this transaction.
622      *
623      * @return the notification message of this transaction
624      */
625     public String getNotificationMessage() {
626         return this.message;
627     }
628
629     /* ============================================================ */
630
631     /**
632      * Subclasses of 'Adjunct' provide data and methods to support one or
633      * more Actors/Operations, but are stored within the 'Transaction'
634      * instance.
635      */
636     public static interface Adjunct extends Serializable {
637         /**
638          * Called when an adjunct is automatically created as a result of
639          * a 'getAdjunct' call.
640          *
641          * @param transaction the transaction containing the adjunct
642          */
643         public default void init(Transaction transaction) {
644         }
645
646         /**
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).
650          */
651         public default void cleanup(Transaction transaction) {
652         }
653     }
654
655     /**
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).
658      *
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)
662      */
663     public <T extends Adjunct> T getAdjunct(final Class<T> clazz) {
664         return clazz.cast(adjuncts.computeIfAbsent(clazz, cl -> {
665             T adjunct = null;
666             try {
667                 // create the adjunct (may trigger an exception)
668                 adjunct = clazz.getDeclaredConstructor().newInstance();
669
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);
674             }
675             return adjunct;
676         }));
677     }
678
679     /**
680      * Explicitly create an adjunct -- this is useful when the adjunct
681      * initialization requires that some parameters be passed.
682      *
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)
686      */
687     public boolean putAdjunct(Adjunct adjunct) {
688         return adjuncts.putIfAbsent(adjunct.getClass(), adjunct) == null;
689     }
690
691     /**
692      * This method needs to be called when the transaction completes, which
693      * is typically right after it is removed from Drools memory.
694      */
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());
700         }
701
702         // iterate over the list
703         for (Adjunct a : values) {
704             try {
705                 // call the 'cleanup' method on the adjunct
706                 a.cleanup(this);
707             } catch (Exception e) {
708                 logger.error("Transaction.cleanup exception", e);
709             }
710         }
711     }
712 }