Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a separate ExecutorService to fix #71. #72

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 64 additions & 96 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.monitorjbl.plugins</groupId>
<artifactId>pr-harmony</artifactId>
<version>2.5.0</version>
<version>2.5.1-SNAPSHOT</version>
<packaging>atlassian-plugin</packaging>

<name>PR Harmony</name>
Expand Down Expand Up @@ -33,64 +33,79 @@
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-api</artifactId>
<scope>provided</scope>
<version>4.11.1</version>
</dependency>
<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-spi</artifactId>
<artifactId>bitbucket-build-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.sal</groupId>
<artifactId>sal-api</artifactId>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-build-api</artifactId>
<artifactId>bitbucket-util</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.atlassian.plugins.rest</groupId>
<artifactId>atlassian-rest-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.sal</groupId>
<artifactId>sal-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.templaterenderer</groupId>
<artifactId>atlassian-template-renderer-api</artifactId>
<version>1.5.7</version>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, if you have a <scope>import</scope> for bitbucket-parent, you shouldn't define explicit versions on your dependencies. Our parent POM defines the versions of the libraries that we actually ship with, so using those versions ensures you're (for example) using the "right" APIs for Bitbucket Server versions you target.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip. The POM was largely copied from Atlassian's example plugin project, so if you haven't already you might want to update that to prevent others from doing this.

<scope>provided</scope>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was unused

<version>2.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
<scope>provided</scope>
</dependency>

<!-- Base test dependencies -->
<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-it-common</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-page-objects</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
Expand All @@ -99,47 +114,9 @@
</exclusions>
<scope>test</scope>
</dependency>

<!-- WIRED TEST RUNNER DEPENDENCIES -->
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced all of these dependencies with new dependencies on Bitbucket Server's common test modules (bitbucket-it-common and bitbucket-page-objects). Those will transitively pull in all the dependencies you need to write browser and REST integration tests.

<dependency>
<groupId>com.atlassian.plugins</groupId>
<artifactId>atlassian-plugins-osgi-testrunner</artifactId>
<version>${plugin.testrunner.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.2-atlassian-1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
<version>2.53.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<testSourceDirectory>${project.basedir}/src/test/java/ut</testSourceDirectory>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than separate the source like this, I've switch the setup to have Surefire (used to run unit tests) ignore everything under src/test/java/it, and have AMPS (which uses Failsafe to run integration tests) ignore everything that's not under src/test/java/it.

(Feel free to not do it this way; this is just how we write our own plugins.)

<plugins>
<plugin>
<groupId>com.atlassian.maven.plugins</groupId>
Expand All @@ -151,64 +128,55 @@
<product>
<id>bitbucket</id>
<instanceId>bitbucket</instanceId>
<version>${bitbucket.version}</version>
<dataVersion>${bitbucket.data.version}</dataVersion>
<version>${bitbucket.test.version}</version>
<dataVersion>${bitbucket.test.version}</dataVersion>
</product>
</products>
<testGroups>
<testGroup>
<id>bitbucket-integration</id>
<includes>
<include>it/**/*.java</include>
</includes>
<productIds>
<productId>bitbucket</productId>
</productIds>
</testGroup>
</testGroups>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
<configuration>
<excludes>
<exclude>it/**/*.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>integration</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>${project.basedir}/src/test/java/it</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

<properties>
<bitbucket.version>5.0.1</bitbucket.version>
<bitbucket.data.version>5.0.1</bitbucket.data.version>
<amps.version>6.3.0</amps.version>
<bitbucket.version>4.8.6</bitbucket.version>
<bitbucket.data.version>${bitbucket.version}</bitbucket.data.version>
<!-- This separate version allows testing the plugin with a different version of the system than
it's compiled against, which is useful for testing compatibility with multiple versions -->
<bitbucket.test.version>${bitbucket.version}</bitbucket.test.version>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd introduced this separate version to allow, for example, compiling PR Harmony against Bitbucket Server 4.8 (which is the lower bound of the compatibility range you've set on Marketplace), but running the tests against Bitbucket Server 5.8 (the upper bound).

You can run the integration tests against a given version with mvn clean install -DskipITs=false -Dbitbucket.test.version=5.8.1

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

<amps.version>6.3.14</amps.version>
<plugin.testrunner.version>1.2.3</plugin.testrunner.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Skip executing integration tests by default. They can be run by adding -DskipITs=false when running Maven -->
<skipITs>true</skipITs>
</properties>
</project>
54 changes: 35 additions & 19 deletions src/main/java/com/monitorjbl/plugins/AsyncProcessor.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,48 @@
package com.monitorjbl.plugins;

import com.atlassian.bitbucket.concurrent.BucketProcessor;
import com.atlassian.bitbucket.util.Version;
import com.atlassian.bitbucket.util.concurrent.ExecutorUtils;
import com.atlassian.sal.api.ApplicationProperties;
import com.atlassian.sal.api.lifecycle.LifecycleAware;
import com.atlassian.util.concurrent.ThreadFactories;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.Serializable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AsyncProcessor {
private static final String PREFIX = "com.monitorjbl.plugins:pr-harmony:";
private final ExecutorService concurrencyService;
public class AsyncProcessor implements LifecycleAware {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LifecycleAware means Bitbucket Server will automatically trigger onStart and onStop at appropriate points. For Bitbucket Server 5.8 and older, this allows onStop to shutdown the PR Harmony-managed ExecutorService.


public AsyncProcessor(ExecutorService concurrencyService) {
this.concurrencyService = concurrencyService;
}
private static final Version VERSION_5_9 = new Version(5, 9);

@SuppressWarnings("unchecked")
public <T extends Serializable> void dispatch(Runnable taskRequest) {
concurrencyService.submit(taskRequest);
}
private final ExecutorService executorService;
private final boolean ownsExecutor;

public static abstract class TaskProcessor<T extends Serializable> implements BucketProcessor<T> {
@Override
public void process(@Nonnull String s, @Nonnull List<T> list) {
list.forEach(this::handleTask);
public AsyncProcessor(ApplicationProperties applicationProperties, ExecutorService executorService) {
ownsExecutor = new Version(applicationProperties.getVersion()).compareTo(VERSION_5_9) < 0;
if (ownsExecutor) {
//For Bitbucket Server versions before 5.9, use our own ExecutorService to avoid starving out hook callbacks
this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(),
ThreadFactories.namedThreadFactory("pr-harmony", ThreadFactories.Type.DAEMON));
} else {
//Bitbucket Server 5.9 includes a fix for BSERV-10652, which moves hook callback to their own threadpool, so
//for 5.9+ use the shared executor
this.executorService = executorService;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 5.9, this allows PR Harmony to continue using our shared ExecutorService. You don't have to do it this way (you could always use your own), but I'd prefer (and appreciate!) it if you did. This way the background processing PR Harmony triggers "shares" the server with other background processing, rather than always running in addition to it. That should help manage overall server load.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not cause thread exhaustion when the server gets a lot of build updates? If the executor wasn't shared, it wouldn't directly interfere with that (although it certainly could interfere with CPU/memory consumption).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a (difficult) balancing act.

Certainly, having a significant number of build status changes could result in PR Harmony creating a lot of tasks for the shared executor. In a system where there are many add-ons using the shared executor, those build status tasks may delay tasks created by other add-ons--just like a large backlog of tasks from other add-ons would be able to delay PR Harmony's build status processing.

For Bitbucket Server, though, that's actually desirable, as long as the tasks being delayed are not critical. That's the real "bug" in BSERV-10652; we were using the shared executor for critical processing: Hook callbacks. Having a shared limit on background processing that can be delayed helps to ensure the accumulation of add-ons doesn't overwhelm the system.

(That's not to say that PR Harmony's build status processing is not important; clearly it is. But a delay of minutes is less critical than with, say, hosting operations.)

Now, it could certainly be contended that if PR Harmony just always used its own executor, it wouldn't starve critical processing, but it also wouldn't starve, or be starved by, other add-ons. From a purely PR Harmony standpoint, that's all good. But from my standpoint, which I'll readily admit is biased, it's less good. The build status processing PR Harmony does is quite heavy, and produces non-trivial system load. When load becomes too high, it's usually not add-on vendors that see the support load, at least initially; it's Bitbucket Server's developers and supporters.

If all add-ons create and use their own executors, it's likely some (most? all?) of them will use non-configurable sizes (as I've done here, I'll note, for 5.8 and earlier). But even if they are configurable, that still means a system administrator has to find out what all those configuration properties are and apply them--and they'd need to have some way of readily determining which add-on(s) executor(s) they need to reconfigure based on who's starving (or who's creating too much load). That's not always straightforward to figure out, especially for non-programmers.

By comparison, if add-ons use the shared executor, it's a single thing to size. That implies, if tasks are getting backed up, administrators can consider their current system load and opt to add more threads to the shared executor, which would then be "spread" across all the add-ons that use it and allow the backlog to be cleared more quickly. Additionally, we're planning to add monitoring for the queue size on the shared executor in a future release of Bitbucket Server, which will make it easier for administrators to track the size of the backlog on their server. That's another feature add-ons which use their own executors won't benefit from. We may add an SPI that they could use (not sure what the plan is there, at the moment), but they'd still have to manually build it out.

}
}

public abstract void handleTask(T task);
public void dispatch(Runnable taskRequest) {
executorService.execute(taskRequest);
}

@Override
public void onStart() {
//No-op
}

@Override
public void onStop() {
if (ownsExecutor) {
ExecutorUtils.shutdown(executorService, LoggerFactory.getLogger(getClass()));
}
}
}
Loading