Strange behaviour with performing transitions programmatically from listener

Background

I want to auto-transition an Issue once the work ration goes above a certain level. Purpose is to move it to a special state that only project manager can move in to and from.

The trigger level is stored in a special issue type called 'Project', only one of these issues exists per project.

In my listener I trigger on any work logged.

Current Code

if(eventTypeId.equals(EventType.ISSUE_WORKLOGGED_ID) || eventTypeId.equals(EventType.ISSUE_WORKLOG_UPDATED_ID)){

log.info("Event caught by DSASystemsListener (Auto hold on overbooked) for issue "+issue.getKey());
			
	//Get issue type
	String type = issue.getIssueTypeObject().getName();
	if(type.equals("Project")){
		return;
	}
		
	//Get issue status
	String status = issue.getStatusObject().getName();
	if(status.matches("Closed|Baselined|Cancelled|Proposed|Open")){
		return;
	}
			
	Issue projectIssue = getProjectIssue(issue);
		
	if(projectIssue == null){
		return;
	}
			
	ApplicationUser pm = getProjectManager(projectIssue);
			
	if(pm == null){
		return;
	}
	CustomField autoHoldTriggerField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("Auto Hold Trigger");
	Double autoHoldTrigger = (Double)projectIssue.getCustomFieldValue(autoHoldTriggerField);
	log.debug("Auto hold trigger: "+autoHoldTrigger);
			
	//Determine if issue is overbooked
	Long origEst = issue.getOriginalEstimate();
	Long timeSpent = issue.getTimeSpent();
	log.debug("Orig: "+origEst+", Time: "+timeSpent);
	if(origEst != null && timeSpent != null){
		float wr = ((float)timeSpent/(float)origEst)*100;
		log.debug("WR: "+wr);
		if(wr > autoHoldTrigger.floatValue()){
			log.info("Issue has work ratio greater than "+autoHoldTrigger+", will hold issue");
			holdIssue(issue, "Issue has been held as work ratio is "+wr, ApplicationUsers.toDirectoryUser(pm));
		}else{
			log.debug("Auto hold trigger not exceeded, return");
			return;
		}
	}else if((origEst == null || origEst == 0) && timeSpent != null && timeSpent > 0){
		log.info("Issue has 0 original estimate and a time spent greater than 0, will hold issue");
		holdIssue(issue, "Issue has been held as time has been booked when Original Estimate is 0", ApplicationUsers.toDirectoryUser(pm));
	}else{
		log.debug("Unknown state, return");
		return;
	}
	
	}		
}
	
private void holdIssue(Issue issue, String comment, User projectManager) {
log.debug("Will hold issue");
	IssueService issueService = ComponentAccessor.getIssueService();
	log.debug("Will validate test if possible transition");
	CommentManager commentManager = ComponentAccessor.getCommentManager();
	commentManager.create(issue, ApplicationUsers.from(projectManager),"AUTO-COMMENT: "+comment,false);
	IssueService.TransitionValidationResult transitionValidationResult  = issueService.validateTransition(projectManager, issue.getId(), 191, issueService.newIssueInputParameters());
	log.debug("Will validate transition result");
	if(transitionValidationResult.isValid()){
		log.debug("Will perform transition");
		IssueService.IssueResult issueResult = issueService.transition(projectManager, transitionValidationResult);
		if(issueResult.isValid()){
			log.debug("Successfully transition issue");
		}else{
			log.error("Failed to transition issue");
		}
	}else{
		List<String> errors = (List<String>)transitionValidationResult.getErrorCollection().getErrorMessages();
		log.debug("Transition not valid");
		for(String error : errors){
			log.debug(error);
		}
	}
}
	
private Issue getProjectIssue(Issue issue){
	Project project = issue.getProjectObject();
	List<Long> issueIds = null;
	try {
		issueIds = (List<Long>)ComponentAccessor.getIssueManager().getIssueIdsForProject(project.getId());
	} catch (GenericEntityException e) {
		e.printStackTrace();
		return null;
	}
	List<Issue> issues = ComponentAccessor.getIssueManager().getIssueObjects(issueIds);
	for(Issue tempIssue : issues){
		if(tempIssue.getIssueTypeObject().getName().equals("Project")){
			return tempIssue;
		}
	}
	return null;
}
	
private ApplicationUser getProjectManager(Issue issue){
	ProjectRoleManager projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager.class);
	ProjectRole projManagerRole = projectRoleManager.getProjectRole("Project Manager");
	ProjectRoleActors projectRoleActors = projectRoleManager.getProjectRoleActors(projManagerRole,issue.getProjectObject());
	Set<ApplicationUser> users = projectRoleActors.getApplicationUsers();
	for(ApplicationUser user : users){
		if(user != null){
			log.debug("Found "+user.getDisplayName()+" to be Project Manager");
			return user;
		}
	}
return null;
}

Sorry for the large amount of code, the private method holdIssue (line 57) is where it all happens.

Problem

I have had a number of weird effects;

  1. If I am defined as the Project Manager (Project Role) AND am the current user AND the issue assignee then this seems to work just fine
  2. If I make another person the Project Manager then it fails and gives an error message about the transition not being valid at this point in the workflow, therefore nothing happens.
  3. If I am current user and another person is the assignee and project manger then the transition happens but in the search view it still shows as 'In Progress' but in the navigator view it shows as 'On Hold'

What I know so far...

In a section of the listener that runs before this code but uses the same objects I am making some changes to the issue, I am updating using the IssueManager (old code) and it was causing problem 3 until I changed the code to include a reindex of the issue.

((MutableIssue)issue).setCustomFieldValue(tpsCustomField, tps);
im.updateIssue(null, (MutableIssue)issue, com.atlassian.jira.event.type.EventDispatchOption.DO_NOT_DISPATCH, false);
try {
	boolean imp = ImportUtils.isIndexIssues();
	ImportUtils.setIndexIssues(true);
	ComponentAccessor.getIssueIndexManager().reIndex(issue);
	ImportUtils.setIndexIssues(imp);
} catch (IndexException e) {
	e.printStackTrace();
}

This fixed problem 3 for a short while; now it is back under the conditions described above.

I am using the IssueService for the transition as I understand that it doesn't require indexing to be cared for.

I feel like this issue is around the fact that I am using a different user to perform the transition rather than the current user. In fact one of my tests I set the logged in user to the project manager via the JiraAuthenticationContext, this seemed to improve things but problem 3 still remained.

This transition is a global transition for the workflow and can be launched from any state. It has a precondition that only a member of the Project Role 'Project Manager' can perform the transition and a validation that a comment must be made. The comment part seems ok.

Questions

What am I doing wrong?!

Am I causing problems modifying the issue in multiple places in the listener using different methods?

Am I handling the users correctly?

NOTE: I tagged scriptrunner because I have used the fast-track post function in the past with great effect and this is more-or-less what I am trying to do here.

** UPDATE **

I replaced the issue manager update code to eliminate that possibility with this...

IssueService issueService = ComponentAccessor.getIssueService();
IssueInputParameters issueInputParameters = issueService.newIssueInputParameters();
issueInputParameters.addCustomFieldValue(tpsCustomField.getIdAsLong(), tps);
IssueService.UpdateValidationResult issueUpdateValidationResult = issueService.validateUpdate(issue.getAssignee(), issue.getId(), issueInputParameters);
if(issueUpdateValidationResult.isValid()){
	issueService.update(issue.getAssignee(), issueUpdateValidationResult);
	log.debug("TPS Source field set to "+tps);
}else{
List<String> errors = (List<String>)issueUpdateValidationResult.getErrorCollection().getErrorMessages();
	log.error("Update of TPS not valid");
	for(String error : errors){
		log.debug(error);
	}
}

The problem still persists...

Thanks in advance!

8 answers

1 accepted

This widget could not be displayed.

Fixed this with two changes, as per @Jamies comment about the re-index in a new thread.

I created a little member for this

private void reindex(){
	Thread thread = new Thread(new Runnable() {
		@Override
		public void run() {
			try {
				Thread.sleep(2000);
				ComponentAccessor.getIssueIndexManager().reIndex(issue);
			} catch (IndexException e) {
				e.printStackTrace();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	});
	thread.start();
}

...then called that after the transition was done successfully...

log.debug("Will perform transition");
IssueService.IssueResult issueResult = issueService.transition(projectManager, transitionValidationResult);
if(issueResult.isValid()){
	log.debug("Successfully transitioned issue");
	reindex();
}else{
	log.error("Failed to transition issue");
}

This fixed issue 3 in my question.

Then to fix issue 2 I had to make the Project Manager the logged in user

ComponentAccessor.getJiraAuthenticationContext().setLoggedInUser(ApplicationUsers.from(projectManager));

Then everything worked great!

Sorry @Jamie, couldn't figure out how to make your comment the answer, but thanks for the input nonetheless

Im going to implement the thread reindex trick, if the IssueService call to reindex fails ... ^^

Hi Mike Wells

I tried your solution but its not working for me. Can you please reply if there is any other work around.

thanks

This widget could not be displayed.

What version of jira?

This widget could not be displayed.

Hi Michael,

about the #3 weird-effect of your problem ... Im having a very similar issue: https://answers.atlassian.com/questions/186657/different-issue-status-shown-between-navigator-and-issue-view-again

(transition from a listener)

This widget could not be displayed.

@Alex - I think if you updated your code to use the IssueService class you should rid yourself of the problem. I believe IssueService was introduce in version 5.

This widget could not be displayed.

Jira is 6.0.1

This widget could not be displayed.

Possible issue that could lead to inconsistant state...

I had a close look at my logging;

Event caught by DSASystemsListener (Auto hold on overbooked) for issue J12345-61

Found Mike Wells to be Project Manager

Auto hold trigger: 170.0

Orig: 57600, Time: 378000

WR: 656.25

Issue has work ratio greater than 170.0, will hold issue

Will hold issue

Will validate test if possible transition

Field layout contains non-orderable field with id 'customfield_10201'.

Will validate transition result

Will perform transition

DSASystemsListener Caught Event: 13

Event caught by DSASystemsListener (Calculate TPS) for issue J12345-61

Time Spent: 378000, Orig: 57600, Work Ratio (long): 656, Work Ration (double): 656.25, Ongoing: false, Perc Comp: 0%

Starting GroovyScript TPSScriptedField

TPS Source = nok.png

TPS Source field set to nok.png

Event caught by DSASystemsListener (Calculate Daily Work Load) for issue J12345-61

Today: 2013-01-09 00:46:00.0

Planned Start: 2013-07-17 00:00:00.0

Planned End: 2013-07-24 00:00:00.0

Due Date: 2013-07-30 00:00:00.0

Successfully transition issue

I think the transition of the issue is causing the firing of an event which in turn is running my listener again. The italic logging is from listener call 1, then the bold logging shows the listener being called again. Finally the last line shows the finished transition. I expect this is not good!

I there a way of de-activating the listener while it is running or will I have to change how my listener is coded.

This widget could not be displayed.

There's something glitchy about reindexing in a listener, but I thought it was fixed in jira 6. I can't find the particular bug right now.

To rule out this problem you could try indexing in a new thread, after a short delay. Basically what might be happening is that you reindex, but when your code is done the original copy of the issue gets indexed, hence the wrong index values.

This widget could not be displayed.

Why don't you just check the action? If it's an action that is invoked by the listener itself, then return. (You may be doing this already, have not read all your code).

Suggest an answer

Log in or Sign up to answer
Atlassian Summit 2018

Meet the community IRL

Atlassian Summit is an excellent opportunity for in-person support, training, and networking.

Learn more
Community showcase
Posted Wednesday in New to Jira

Are you planning to trial, or are currently trialling Jira Software? - We want to talk to you!

Hello! I'm Rayen, a product manager at Atlassian. My team and I are working hard to improve the trial experience for Jira Software Cloud. We are interested in   talking to 20 people planning t...

155 views 2 0
Join discussion

Atlassian User Groups

Connect with like-minded Atlassian users at free events near you!

Find a group

Connect with like-minded Atlassian users at free events near you!

Find my local user group

Unfortunately there are no AUG chapters near you at the moment.

Start an AUG

You're one step closer to meeting fellow Atlassian users at your local meet up. Learn more about AUGs

Groups near you