Is there hard-coded timeout setting for gadget?

We have a new custom plugin trying to display total active JIRA and Confluence users with their groups, and total users who access JIRA and Confluence last month. Our JIRA and confluence servers are using 2000 user licenses. all users are managed by Crowd. To get the information, the plugin query mysql database directly. the plugin works well on a test server with less users, but when deployed to the production server, the gadget timed out after 1 min.

We also have another plugin call JIRA API directly to display project and user status pie chart. when I add more than one this gadget to a dashborad, the pie chart gadget also timeout in 1 min. I opened a JIRA support ticket GHS-106996, and it can be reproduce on atlassian side too. Atlassian support told me come here for advice.

Would you please post the JavaScript code that your gadget uses to make the HTTP request to your Jira Server?

I don’t know that this is the cause of the problem, but you should read about Making Ajax calls from Atlassian gadgets.

=============script source===============================

<?xml version="1.0" encoding="UTF-8" ?>
    <Require feature="dynamic-height"/>
    <Require feature="oauthpopup"/>
    <Require feature="setprefs"/>
    <Require feature="settitle"/>
    <Require feature="views"/>
    <Optional feature="atlassian.util" />
	
	<Optional feature="gadget-directory"> 
		<Param name="categories"> 
			JIRA 
		</Param> 
	</Optional>
	
    #oauth
    
    #supportedLocales("gadget.common,gadget.lenovopiechart")
    #supportLocales

</ModulePrefs>

<UserPref name="isConfigured" datatype="hidden" default_value="false" />
<UserPref name="projectOrFilterId" datatype="hidden"/>
<UserPref name="refresh" datatype="hidden" default_value="false" />

<Content type="html" view="profile,canvas,home,default"><![CDATA[

	#requireResource("com.atlassian.gadgets.publisher:ajs-gadgets")
    #requireResource("com.atlassian.jira.gadgets:jira-global")
    #requireResource("com.atlassian.jira.gadgets:autocomplete")
	#includeResources()
	
	<style type="text/css">
		.headerColor {
			background-color:#DDFADE;
		}
		
		#lenovoPieChart th,#lenovoPieChart td {
	    	padding:10px;
	    }
	</style>

	<script type="text/javascript">
	(function () {
		var gadget = AJS.Gadget({
			baseUrl: "__ATLASSIAN_BASE_URL__",
			useOauth: "__ATLASSIAN_BASE_URL__/rest/gadget/1.0/currentUser",
            config: {
                descriptor: function(args)
                {
                    var gadget = this;
                    
                    gadgets.window.setTitle("__MSG_gadget.lenovopiechart.configtitle__");
                    
                    var projectOptionsLength = args.projectOptions.length;
                    
                    var projectOptionsArray = [];
                    for(var i=0;i<projectOptionsLength;i++) {
                    	projectOptionsArray.push({"label":args.projectOptions[i].name,"value":args.projectOptions[i].key+","+args.projectOptions[i].id});
                    
                    }
                    
                    var projectOptionSelectBox = {"label":"Projects","options":projectOptionsArray};
                    var projectPicker = AJS.gadget.fields.projectPicker(gadget, "projectOrFilterId", projectOptionSelectBox);

                    return {
                    	action: "__ATLASSIAN_BASE_URL__/rest/lenovopiechart/1.0/PieChartGenerator/validate",
                        fields: [
                        	projectPicker,
                            AJS.gadget.fields.nowConfigured()
                        ]
                    };
                },
                args: [
           		 	 {
                             key: "projectOptions",
                             ajaxOptions: function () {
	             		    	return {
	                		        url: "__ATLASSIAN_BASE_URL__/rest/api/2/project",
	                		        contentType: "application/json",
									dataType: "json"
	              		        };
	             		   	}
                     } 
       		 	 ]
            },
			view: {
				enableReload: true,
                onResizeReload: true,
     		    template: function(args) {
        		    var gadget = this;
                    var combinedData = [];
                    var userNamesLength=args.userNamesByProject.length;
                    for(var i=0;i<userNamesLength;i++) { 
                   		  combinedData.push({fullName:args.userNamesByProject[i].displayName,shortName:args.userNamesByProject[i].key});                      		    	
                   	}
                   	
               		var dataToSend = {pieChartDataList:combinedData};
               		AJS.$.ajax({
									url: "/rest/lenovopiechart/1.0/PieChartGenerator/generatePieChartData",
									data:{
                                    	filterData:JSON.stringify(dataToSend),
                                    	filterId:gadgets.util.unescapeString(this.getPref("projectOrFilterId")).split(",")[1],
                                    	filterType:gadgets.util.unescapeString(this.getPref("projectOrFilterId")).split(",")[1],
                                    	baseUrl:"__ATLASSIAN_BASE_URL__"
                                    },
									type: "POST",
									dataType: "json",
									async: true,
									success: function (responseData) {
										AJS.$("body").removeClass("loading");
										var pieChartDiv = AJS.$("<div>");
										pieChartDiv.append("<img src='"+responseData.pieChartImage+"'>");
										var table=AJS.$("<table border='1' id='lenovoPieChart' cellspacing='4' style='margin:4px auto'>");
										table.append("<tr class='headerColor'><th>Name</th><th>Total</th></tr>");
										for(var i=0;i<responseData.pieChartDataList.length;i++) {
										    if((responseData.pieChartDataList[i].total)!=0)
										    {
											table.append("<tr><td>"+responseData.pieChartDataList[i].fullName+"</td><td>"+responseData.pieChartDataList[i].total+"</td></tr>");
											}
										}
										table.append("</table>");
										pieChartDiv.append(table);
										pieChartDiv.append("</div>");
									    gadget.getView().html(pieChartDiv);
									},
									error: function(response) {
							            
									}
						});
						
				   if(!AJS.$("body").hasClass("loading")) {
				   	AJS.$("body").addClass("loading");
				   }
                          		    
         	 	    
       			 },
       		 	 args: [ {
                        key: "userNamesByProject",
                        ajaxOptions: function ()
                        {
                            var gadget = this;
                            return {
                                url: "/rest/api/2/user/assignable/search",
                                data:  {
                                    project : gadgets.util.unescapeString(this.getPref("projectOrFilterId")).split(",")[0]
                                },
                                contentType: "application/json",
								dataType: "json"
                            };
                        }
                    }]
   		 	}
	 	});
	 	gadgets.window.adjustHeight();
	})();
    </script>

]]></Content>

==============java source======================
@Path("/PieChartGenerator")
public class PieChartGenerator {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PieChartGenerator.class);
private final ChartUtils chartUtils;
public PieChartGenerator(ChartUtils chartUtils) {
this.chartUtils=chartUtils;
}

private static PieChartFilter convertStringToObject(String jsonString) {
	if (jsonString != null) {
		ObjectMapper mapper = new ObjectMapper();
		try {
			return mapper.readValue(jsonString, PieChartFilter.class);
		} catch (Exception e) {
		}
	}
	return null;
}

@GET
@Path("validate")
public Response validate(
		@QueryParam("projectOrFilterId") String projectOrFilterId
		) {
	ErrorCollection errorCollection = new ErrorCollection();
	if (projectOrFilterId==null||projectOrFilterId.trim().length()==0) {
		ValidationError validationError = new ValidationError(
				"projectOrFilterId",
				"No Filter selected"
		);
		errorCollection.addError(validationError);
	}
	if (errorCollection.hasErrors()) {
		return Response.status(Response.Status.BAD_REQUEST).entity(errorCollection).build();
	}
	return Response.ok().build();
}


@POST
@Path("generatePieChartData")
public Response generateTableData(@FormParam("filterData") String filterData, @FormParam("filterId") String filterId, @FormParam("filterType") String filterType,@FormParam("baseUrl") String baseUrl,@Context HttpServletRequest request) {
	
	PieChartFilter pieChartFilter = convertStringToObject(filterData);
	
	Set<PieChartData> pieChartDataSet=pieChartFilter.getPieChartDataList();
	CloseableHttpClient client = null;
	HttpResponse response=null;
	InputStreamReader inputStream = null;
	BufferedReader bufferReader = null;
	String chartString = null;
	try {
		StringBuffer jsessionValue = new StringBuffer();
		if(request.getCookies()!=null && request.getCookies().length>0) {
			for(int i = 0; i < request.getCookies().length; i++) {
				Cookie c=request.getCookies()[i];
				String name = c.getName();
				String value = c.getValue();
				if(name.equalsIgnoreCase("JSESSIONID")) {
					jsessionValue.append("JSESSIONID");
					jsessionValue.append("=");
					jsessionValue.append(value);
					break;
				}
			}
		}
		client = HttpClients.createDefault();
		DefaultPieDataset dataset = new DefaultPieDataset();
		for(PieChartData pieChartData:pieChartDataSet) {
			String s = "project="+filterId+" AND (reporter was "+pieChartData.getShortName()+" or assignee was "+pieChartData.getShortName()+")";
			HttpGet getRequest = new HttpGet(baseUrl+"/rest/api/2/search?jql="+URLEncoder.encode(s,"UTF-8")+"&maxResults=-1");
			getRequest.addHeader("cookie", jsessionValue.toString());
			List<NameValuePair> queryParamList = new ArrayList<NameValuePair>();
			
			NameValuePair jql = new BasicNameValuePair("jql",URLEncoder.encode(s, "UTF-8"));
			queryParamList.add(jql);
			NameValuePair maxResults = new BasicNameValuePair("maxResults","-1");
			queryParamList.add(maxResults);
			response = client.execute(getRequest);
			inputStream = new InputStreamReader(response.getEntity().getContent(),"UTF-8");
			bufferReader = new BufferedReader(inputStream);
			StringBuffer result = new StringBuffer();
			String line = "";
			while ((line = bufferReader.readLine()) != null) {
				result.append(line);
			}
			try {
				JSONObject json = (JSONObject)new JSONParser().parse(result.toString());
				Long total=(Long) json.get("total");
				pieChartData.setTotal(total.intValue());
				if(total.intValue()!=0)
				{
					
					dataset.setValue(pieChartData.getFullName(), total.intValue());
				}
			
			} catch(Exception e) {
				pieChartData.setTotal(0);
				//dataset.setValue(pieChartData.getFullName(), 0);
			}
			
				
		}
		
		
		
		
		JFreeChart chart = ChartFactory.createPieChart(
		        "Pie Chart",
		        dataset,
		        true, 
		        true,
		        false);
	   chartString=chartUtils.renderBase64Chart(chart.createBufferedImage(600, 800), "MyChart");
	   pieChartFilter.setPieChartImage(chartString);
		
	} catch(Exception e) {
		StringWriter writer = new StringWriter();
		e.printStackTrace(new PrintWriter(writer));
		return Response.ok(writer.toString()).build();
	} finally {
		if(client!=null) {
			try {
				client.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	return Response.ok(pieChartFilter).build();
}

}

Where do you see the timeout message? Do you see an HTTP 408 (Request timeout) response?

on a large JIRA project with many users, our gadget changed to a blank screen after showing progress bar for a min; but works fine on a small project with only several users. can be recreated on http too. I don’t see any timeout, only see the status of generatePieChartData is Cancelled from F12.

Let’s gather some more information about the problem. Watch the Network panel in your browser’s developer tools panel. What status and body content are in the response from your /generatePieChartData end-point.

Also put some code inside the error: function to report on the contents of the response.

Hi David,

we added code to the error section, but never showed up, timeout. based on [JRASERVER-60212] Increase plugin default timeout to 300 seconds - Create and track feature requests for Atlassian products. , I tried -Datlassian.plugins.enable.wait=300, but didn’t work, the timeout is still 60 seconds.

Regards,
Jun Li

What did show up in the error section?

Can you post here the contents of the HTTP response? The Network panel in your browser’s developer tools will show you that. Alternatively, you could use a debugging proxy like Charles or Fiddler.

the HTTP response is empty from the Network panel.
from fiddler, I can see the code is 408. and “The request body did not contain the specified number of bytes. Got 0, expected 693”
after enable profiling and sql logging, there are no errors/exceptions in log files. look like the operation was cut off while it was still going, or timed out after 60 sec.

There’s some useful information there. It suggests that the client code is telling the server to expect a request containing 693 bytes, but the server is not receiving any bytes.

Your gadget’s JavaScript makes HTTP requests to several end-points:

/rest/api/2/user/assignable/search
/rest/api/2/project
/rest/lenovopiechart/1.0/PieChartGenerator/generatePieChartData

On which of those URLs is the timeout happening?

I notice that you specify the base URL when you create the gadget object:

var gadget = AJS.Gadget({
    baseUrl: "__ATLASSIAN_BASE_URL__",
    ...
});

Since the framework uses this URL for prefixing relative Ajax requests, you should not include __ATLASSIAN_BASE_URL__ when you specify the other URLs. For example, do this:

useOauth: "/rest/gadget/1.0/currentUser",

instead of:

useOauth: "__ATLASSIAN_BASE_URL__/rest/gadget/1.0/currentUser",

You retrieve the project options like this:

args: [{
    key: "projectOptions",
    ajaxOptions: function() {
        return {
            url: "__ATLASSIAN_BASE_URL__/rest/api/2/project",
            contentType: "application/json",
            dataType: "json"
        };
    }
}]

You should remove __ATLASSIAN_BASE_URL__ from the url parameter value. Also, remove the contentType parameter - it is required only when you are sending JSON data to the server in the request. It might even be the cause of the 408 timeout. In fact, I’d suggest using a simple string, like so:

args: [{
    key: "projectOptions",
    ajaxOptions: "/rest/api/2/project"
}]

HI David

In the below code

args: [{
key: “projectOptions”,
ajaxOptions: function() {
return {
url: “ATLASSIAN_BASE_URL/rest/api/2/project”,
contentType: “application/json”,
dataType: “json”
};
}
}]

If we remove “ATLASSIAN_BASE_URL” or contentType: “application/json”,dataType: “json” plugin is failed to load configuration screen.

Let’s try to determine why that happens. Here’s what the Atlassian documentation says about setting the baseUrl:

“baseUrl – A required option used to pass the base URL to the framework. The framework uses this URL for prefixing relative Ajax requests and also makes it available from gadget.getBaseUrl() and AJS.gadget.getBaseUrl().”

I can confirm that it works as expected when the gadget is properly configured. For example, we have a gadget that is like this:

AJS.Gadget({
    baseUrl: "__ATLASSIAN_BASE_URL__",
    useOauth: "/rest/gadget/1.0/currentUser",
    config: {
        args: [{
            key: 'settings',
            ajaxOptions: '/rest/projectbalm/riskregister/latest/settings'
        }],
        ...
    },
    ...
);}

To determine what is going on with your gadget, please use relative URLs as above, then check what HTTP requests get sent to the server (using Fiddler or similar). Where are the requests sent?