Infinite loop - stackoverflow exception jira scriptrunner

Mouna Hammoudi
Contributor
August 15, 2022

I have an infinite loop in my code at the level of lines 125 and 133. More specifically, these are lines

   ComponentAccessor.getIssueService().transition(mycurrentUser, myresult2)

and line

IssueService.TransitionValidationResult myresult = ComponentAccessor.getIssueService().validateTransition(mycurrentUser, issue.getId(), transitionToBeDoneInt, inputParameters);

Here is my full code below. Anyone knows what is causing this infinite loop? I have looked at my code many times and I really don't understand what's wrong. I am using Jira and scriptrunner for a postfunction which is supposed to add a new comment.

this code has nothing recursive in it. It is simply supposed to do a transition and that's it, NO RECURSION. The error is so weird because I am having an infinite loop even though the code is not presenting any signs of recursion.

My code is very simple, it only needs to execute 1 transition and write a comment, no recursion and no loops are required in my code. 

 

import com.opensymphony.workflow.WorkflowContext
import com.atlassian.jira.issue.Issue;
import java.util.List;
import org.apache.log4j.Logger;
import com.atlassian.jira.issue.ModifiedValue;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.component.ComponentAccessor
import org.ofbiz.core.entity.GenericDelegator;
import com.atlassian.jira.issue.changehistory.ChangeHistoryManager;
import com.atlassian.jira.issue.history.ChangeItemBean;
import com.atlassian.jira.issue.comments.Comment
import com.atlassian.jira.workflow.JiraWorkflow
import com.atlassian.jira.workflow.WorkflowManager
import com.atlassian.jira.issue.changehistory.ChangeHistoryItem
import com.onresolve.scriptrunner.runner.util.UserMessageUtil
import com.atlassian.jira.config.SubTaskManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.link.LinkCollectionImpl;
import com.atlassian.jira.issue.link.IssueLink;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.jira.issue.comments.CommentManager;
import com.atlassian.jira.issue.link.IssueLinkManager;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.ErrorCollection;
import com.atlassian.jira.util.JiraUtils;
import com.atlassian.jira.workflow.JiraWorkflow;
import com.atlassian.jira.workflow.WorkflowManager;
import com.atlassian.jira.workflow.WorkflowTransitionUtil;
import com.atlassian.jira.workflow.WorkflowTransitionUtilImpl;
import com.atlassian.jira.workflow.function.issue.AbstractJiraFunctionProvider;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.workflow.WorkflowException;
import com.opensymphony.workflow.loader.StepDescriptor;
import com.opensymphony.workflow.spi.SimpleStep;
import com.atlassian.jira.config.ConstantsManager
import com.atlassian.jira.workflow.ImmutableWorkflowDescriptor
import com.atlassian.jira.web.action.admin.workflow.ViewWorkflowStep
import com.opensymphony.workflow.loader.ActionDescriptor
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.issue.IssueInputParameters
import com.atlassian.jira.bc.issue.IssueService.IssueValidationResult
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.workflow.WorkflowTransitionUtil
import com.atlassian.jira.workflow.WorkflowTransitionUtilFactory
import com.atlassian.jira.workflow.TransitionOptions
import com.atlassian.jira.bc.issue.IssueService
import com.atlassian.jira.bc.issue.IssueService.TransitionValidationResult

def log = Logger.getLogger("atlassian-jira.log")
log.warn("This is the last action ")

WorkflowManager workflowManager = ComponentAccessor.getWorkflowManager();
JiraWorkflow workflow = workflowManager.getWorkflow(issue);

def wfd = workflow.getDescriptor()
def actionName = wfd.getAction(transientVars["actionId"] as int).getName(); 

//Get worklflowname By actionId
int transitionToBeDone = transientVars["actionId"] as int;


 log.warn("CAM 1 "+ transitionToBeDone)

 
ApplicationUser mycurrentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
log.warn("CAM 2")


log.warn("CAM 3")
  int transitionToBeDoneInt = transitionToBeDone as Integer
        
List currentSteps = (ArrayList)transientVars.get("currentSteps");
log.warn("CAM 4 "+currentSteps)

SimpleStep simpleStep = currentSteps.get(0);
log.warn("CAM 5 "+simpleStep)


int beforeStepId = simpleStep.getStepId();
log.warn("CAM 6 "+beforeStepId)

ActionDescriptor ad = workflow.getDescriptor().getAction(transitionToBeDoneInt);
int afterStep = ad.getUnconditionalResult().getStep();
log.warn(" cam 7 "+afterStep)
log.warn( "resolved transition target last "+ComponentAccessor.workflowManager.getNextStatusIdForAction(issue , transientVars['actionId'] as int) )

 targetStatus=ComponentAccessor.workflowManager.getNextStatusIdForAction(issue ,transitionToBeDoneInt)
log.warn(" cam 8")

def constantsManager = ComponentAccessor.getConstantsManager()
log.warn(" cam 9")

def statusName=constantsManager.getStatus(targetStatus).getName();
log.warn(" cam 10")

log.warn("cam  transition target "+targetStatus + "status name "+statusName)
 log.warn("CAM 11")




    log.warn("cam 12")

log.warn("cam 13")

    
def mounaComment= "+++ added via workflow action "+"\""+actionName+"\"+++"
log.warn("cam 12"+mounaComment)

String content = (transientVars["comment"] +"\n"+mounaComment ) as String
log.warn("cam 13"+ content)
transientVars["comment"]= content
//inputParameters.setComment(content)
log.warn("cam 14"+ content)



log.warn("CAM 15 "+ issue.getKey())


def inputParameters = ComponentAccessor.getIssueService().newIssueInputParameters();
log.warn("CAM 16 "+ mycurrentUser+" "+issue.getId()+" "+ transitionToBeDoneInt+" "+inputParameters)

IssueService.TransitionValidationResult myresult = ComponentAccessor.getIssueService().validateTransition(mycurrentUser, issue.getId(), transitionToBeDoneInt, inputParameters);
def myresult2= myresult; 
 log.warn("CAM 16 "+ issue.getKey())

try {
    if(myresult2.isValid()) {
    log.warn("CAM 17 "+mycurrentUser+" ")

    ComponentAccessor.getIssueService().transition(mycurrentUser, myresult2)
     
}

} catch(Exception e) {
        log.warn("CAM 18 "+e)

      log.warn (result.getErrorCollection().getErrors());

}
    

Here is a snippet of what is printed in my error log

Capture.PNG

2 answers

2 votes
Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 15, 2022

It is not your code, it is what you are trying to do with it.

As we said last week on https://community.atlassian.com/t5/Jira-Software-questions/Status-is-not-updated-after-performing-transitionResult/qaq-p/2106529 do not transition an issue in a post function.

The recursion is happening because the code you are running is run inside a transitition and because it triggers the same transition, it runs it again, to trigger the same tansition which runs it again to ....  

You really need to stop trying to do this.  Use the fast-track transition function in Scriptrunner, or do it as a listener.

Mouna Hammoudi
Contributor
August 15, 2022

I am trying to follow your advice and use a listener instead but I need to retrieve the transition name to only fire the event if the transition is called "MounaTransition". It seems that the following code is not working to get the transition name in the listener. 

 import com.atlassian.jira.component.ComponentAccessor
def workflow = ComponentAccessor.getWorkflowManager().getWorkflow(issue)
def wfd = workflow.getDescriptor()
def actionName = wfd.getAction(transientVars["actionId"] as int).getName()

 Here is the code I have written so far in my listener to try to do the same thing that I was doing with the custom post function before. Any idea on how to get the transition name/ID? I can only operate and execute my script if the tranition name matches the one I am interested in. 

import org.ofbiz.core.entity.GenericValue

import com.opensymphony.workflow.WorkflowContext

import com.atlassian.jira.issue.Issue;

import java.util.List;

import org.apache.log4j.Logger;

import com.atlassian.jira.issue.ModifiedValue;

import com.atlassian.jira.issue.fields.CustomField;

import com.atlassian.jira.component.ComponentAccessor

import org.ofbiz.core.entity.GenericDelegator;

import com.atlassian.jira.issue.changehistory.ChangeHistoryManager;

import com.atlassian.jira.issue.history.ChangeItemBean;

import com.atlassian.jira.issue.comments.Comment

import com.atlassian.jira.workflow.JiraWorkflow

import com.atlassian.jira.workflow.WorkflowManager

import com.atlassian.jira.issue.changehistory.ChangeHistoryItem

import com.onresolve.scriptrunner.runner.util.UserMessageUtil

import com.atlassian.jira.config.SubTaskManager

import com.atlassian.jira.issue.Issue

import com.atlassian.jira.issue.link.LinkCollectionImpl;

import com.atlassian.jira.issue.link.IssueLink;

import com.atlassian.crowd.embedded.api.User;

import com.atlassian.jira.issue.comments.CommentManager;

import com.atlassian.jira.issue.link.IssueLinkManager;

import com.atlassian.jira.user.ApplicationUser;

import com.atlassian.jira.util.ErrorCollection;

import com.atlassian.jira.util.JiraUtils;

import com.atlassian.jira.workflow.JiraWorkflow;

import com.atlassian.jira.workflow.WorkflowManager;

import com.atlassian.jira.workflow.WorkflowTransitionUtil;

import com.atlassian.jira.workflow.WorkflowTransitionUtilImpl;

import com.atlassian.jira.workflow.function.issue.AbstractJiraFunctionProvider;

import com.opensymphony.module.propertyset.PropertySet;

import com.opensymphony.workflow.WorkflowException;

import com.opensymphony.workflow.loader.StepDescriptor;

import com.opensymphony.workflow.spi.SimpleStep;

import com.atlassian.jira.config.ConstantsManager

import com.atlassian.jira.workflow.ImmutableWorkflowDescriptor

import com.atlassian.jira.web.action.admin.workflow.ViewWorkflowStep

import com.opensymphony.workflow.loader.ActionDescriptor

import com.atlassian.jira.issue.IssueInputParametersImpl

import com.atlassian.jira.issue.IssueInputParameters

import com.atlassian.jira.bc.issue.IssueService.IssueValidationResult

import com.atlassian.jira.user.ApplicationUser

import com.atlassian.jira.workflow.WorkflowTransitionUtil

import com.atlassian.jira.workflow.WorkflowTransitionUtilFactory

import com.atlassian.jira.workflow.TransitionOptions

import com.atlassian.jira.bc.issue.IssueService

import com.atlassian.jira.bc.issue.IssueService.TransitionValidationResult

import com.atlassian.event.Event

import com.atlassian.jira.event.issue.AbstractIssueEventListener;

import com.atlassian.jira.event.issue.IssueEvent;

def log = Logger.getLogger("atlassian-jira.log")

log.warn("This is the last action ")

Issue issue = event.getIssue();

 log.warn("CAM 0 ")

 log.warn("CAM 0.1 ")

//Get worklflowname By actionId

  GenericValue changeLog = event.getChangeLog();

                log.warn("HERE 1")

       

        List<GenericValue> changeItems = ComponentAccessor.getChangeHistoryManager().getAllChangeItems(issue)

              //  changeItems = changeLog.internalDelegator.findByAnd("ChangeItem", fields);

            //changeItems = changeLog

                log.warn("HERE 5::: "+ changeItems)

        log.warn("MOUNA IS HERE AFTER HERE 5 " +changeItems.size());

        log.warn("VALUES 1");

      int lastIndex= changeItems.size()-1;

 log.warn("CAM 0.2")

 ChangeHistoryItem value= changeItems.get(lastIndex) as ChangeHistoryItem;

 def workflow = ComponentAccessor.getWorkflowManager().getWorkflow(issue)

def wfd = workflow.getDescriptor()

      log.warn("GENERIC VALUE "+ transientVars["actionId"] +" ");

def actionName = wfd.getAction(transientVars["actionId"] as int).getName()

      log.warn("GENERIC VALUE "+ actionName +" ");
Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 16, 2022

This is the wrong way to do this.

Listeners listen for events, and with scripted listeners, you can choose which events you want to listen for.

Instead of trying to work out what transition has been run and match it to a name (that could change), use the event system.

Create a custom event (Admin -> Issues -> Events)

Edit the workflow, finding the transition(s) you want to run this code for, and look at the post-functions for the transition.  Change the one that says "fire event:" so that it fires your custom event.

Now your listener will run when you need it to.

(I'd also strongly recommend looking at the fast-track transitions, they'll need even less coding to use)  

Mouna Hammoudi
Contributor
August 16, 2022

I have created an event, but how can I specify  the associated workflows for it like shown in the picture below. I don't know how to specify the wlorkflows. Also, in my event listener code, how can I access the transition name? transientVars["actionId"] is not working, what's the workaround for this?

Capture.PNG

Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 16, 2022

I think you've misunderstood this.  The whole point of the event is that you don't need to look at the transition.  If that event is fired, you know it's come from the chosen transition(s)

Edit the workflow, finding the transition(s) you want to run this code for, and look at the post-functions for the transition.  Change the one that says "fire event:" so that it fires your custom event.

Mouna Hammoudi
Contributor
August 16, 2022

yes but I need to print a comment and I would like to retrieve the transition that was applied automatically. 

def mounaComment= "+++ added via workflow action "+"\""+actionName+"\"+++"

Also, I need to execute the transition myself through this code so I need to retrieve the ID of the transition and execute it for the 2 code snippets I have just pasted. 

def inputParameters = ComponentAccessor.getIssueService().newIssueInputParameters();
log.warn("CAM 16 "+ mycurrentUser+" "+issue.getId()+" "+ transitionToBeDoneInt+" "+inputParameters)

IssueService.TransitionValidationResult myresult = ComponentAccessor.getIssueService().validateTransition(mycurrentUser, issue.getId(), transitionToBeDoneInt, inputParameters);
def myresult2= myresult; 
 log.warn("CAM 16 "+ issue.getKey())
Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 16, 2022

You don't need to re-read the transition to make a comment.

You already know what transition was triggered - the one that fired your custom event.

Mouna Hammoudi
Contributor
August 17, 2022

I have tried the following code. It is using workflowTransitionUtil.setAction, workflowTransitionUtil.validate and workflowTransitionUtil.progress and it also fails. I am getting the errror: 

Caused by: org.ofbiz.core.entity.GenericTransactionException: Commit failed, rollback previously requested by nested transaction.
com.atlassian.jira.transaction.TransactionRuntimeException: org.ofbiz.core.entity.GenericTransactionException: Commit failed, rollback previously requested by nested transaction.
org.ofbiz.core.entity.GenericTransactionException: Commit failed, rollback previously requested by nested transaction.
2022-08-17 11:31:55,313+0200 https-openssl-nio-443-exec-533 ERROR mouh 691x78439x26 1a2i7f4 10.248.140.0 /secure/CommentAssignIssue.jspa [c.a.jira.transaction.TransactionSupportImpl] Unable to commit transaction : Commit failed, rollback previously requested by nested transaction.

Here is the code. I had hope it would work since it is operating in a different way than my old code. Could you please let me know about the solution that you mean and write down the code? I would like to execute the transition even though it is leading to itself. 

import com.atlassian.jira.workflow.WorkflowTransitionUtil
import com.atlassian.jira.issue.comments.Comment
import com.atlassian.jira.workflow.JiraWorkflow
import com.atlassian.jira.workflow.WorkflowManager
import org.apache.log4j.Logger
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.workflow.WorkflowTransitionUtilImpl
import com.atlassian.jira.util.JiraUtils
import com.atlassian.jira.user.ApplicationUser;

def log = Logger.getLogger("atlassian-jira.log")
log.warn("test")

WorkflowManager workflowManager = ComponentAccessor.getWorkflowManager();
JiraWorkflow workflow = workflowManager.getWorkflow(issue);

def wfd = workflow.getDescriptor()
def actionName = wfd.getAction(transientVars["actionId"] as int).getName();
ApplicationUser mycurrentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

log.warn("This is the last action done "+actionName)

log.warn("CAM 2")
int transitionToBeDoneInt = 1731 //ID of MounaTransition

log.warn("cam 3 " +transitionToBeDoneInt)

def myComment= "+++ added via workflow action "+actionName+"\"+++"
log.warn("cam 4 "+actionName)

def content =( transientVars["comment"] +"\n"+myComment )as String
log.warn("cam 5 "+ content)

transientVars["comment"]= content

log.warn("cam 6"+ content)


log.warn("CAM 7 "+ issue.getKey())


def inputParameters = ComponentAccessor.getIssueService().newIssueInputParameters();

log.warn("CAM 8 "+ " "+issue.getId()+" "+ transitionToBeDoneInt+" "+inputParameters)

def workflowTransitionUtil = ( WorkflowTransitionUtil ) JiraUtils.loadComponent( WorkflowTransitionUtilImpl.class )
workflowTransitionUtil.setIssue(issue)
workflowTransitionUtil.setUserkey(mycurrentUser.getKey())

workflowTransitionUtil.setAction(transitionToBeDoneInt);//Id of the status you want to transition to
log.warn("CAM 9 "+ " "+issue.getId()+" "+ transitionToBeDoneInt+" "+inputParameters)
workflowTransitionUtil.validate()
log.warn("CAM 19 "+ " "+issue.getId()+" "+ transitionToBeDoneInt+" "+inputParameters)

workflowTransitionUtil.progress();
log.warn("CAM 11 "+ " "+issue.getId()+" "+ transitionToBeDoneInt+" "+inputParameters)
Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 17, 2022

I'm not sure how else to explain this.

You can not run a transition inside itself.  

So, you move to a listener, that's fine. 

But you are still creating an infinite recursion.

You are trying to trigger the same transition, which will trigger the same transition, which will trigger the same transition...  As these start to stack up, you'll find  events are being fired faster than Jira can process them, so transitions start to be started while others are running, so you're getting recursive errors.

I strongly recommend that you do this:

1. Add a post-function to the transition that sets a (hidden if you want) field.  Let's call it "I have run" and it could be of any type

2. Use a fast-track transition instead of trying to write your own.  See https://docs.adaptavist.com/sr4js/latest/features/workflows/post-functions/built-in-post-functions/fast-track-transition-an-issue

3. In the conditions to execute in the fast-track, put in at least "only run if "I have run" is empty"

Mouna Hammoudi
Contributor
August 17, 2022

Why adding a post-function to the transition that sets a (hidden if you want) field.  My original code is already running inside a postfunction. Why would I add a second one? Adding a second postfunction would not change the fact that I am transitioning from the same status to itself. So how would it solve the problem?

Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 17, 2022

Yet again, you must not transition an issue during a transition.

You need the flag to be set the first time you run the transition, so that you can detect it and not run again.  The transition has to finish before the flag will be set properly, the next execution of the transition has to be done after completion so the flag is there.

Mouna Hammoudi
Contributor
August 17, 2022

can you please write some code to let me know what to do? I am kind of lost. How can I stop the transition from executing with the flag?

Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 17, 2022

Ok,

https://library.adaptavist.com/entity/change-the-value-of-a-custom-field-in-a-post-function gives you "how to change a custom field in a post-function"

Then, when you configure your fast-track transition, you can read the field in the conditions for it (see https://docs.adaptavist.com/sr4js/latest/features/workflows/post-functions/built-in-post-functions/fast-track-transition-an-issue )

Mouna Hammoudi
Contributor
August 17, 2022

It will sill transition to itself, isn't that a problem?

Nic Brough -Adaptavist-
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 17, 2022

Yes.

That is what I've been saying throughout - the whole problem is that it will transition to itself.

Trying to recursively call a transition is the problem.  You need a way to break out of that recursion.

0 votes
Matthias Gaiser _K15t_
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
August 15, 2022

Hi @Mouna Hammoudi

I'm not sure if I got your full use case, but maybe I can ask some questions or give you pointers to help you.

In the script, you perform a transition - will this trigger the same postfunction to be called? If yes, make sure that you properly exclude this post function (by removing it from the transition or specifying a corresponding if condition.

Alternatively, I wonder why you're using Scriptrunner and if not Jira Automation could be a simpler solution for you.

Cheers,
Matthias.

Mouna Hammoudi
Contributor
August 15, 2022

No this code has nothing recursive in it. It is simply supposed to do a transition and that's it, NO RECURSION. The error is so weird because I am having an infinite loop even though the code is not presenting any signs of recursion.

My code is very simple, it only needs to execute 1 transition and write a comment, no recursion and no loops are required in my code. 

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PRODUCT PLAN
PREMIUM
TAGS
AUG Leaders

Atlassian Community Events