Only execute single test task in parallel build

Hi,

I am trying to improve the build time of Hibernate ORM by using a parallel build, but since we are using a shared database for tests, I am running into issues when two test tasks of different projects run in parallel.

I read that this could be solved by using a shared build service and setting maxParallelUsage to 1 which I tried, but now my build fails, saying:

Cannot query the value of task ‘:hibernate-hikaricp:test’ property ‘databaseService’ because it has no value available.

I am using a custom plugin that defines the build service:

public class HibernateBuildPlugin implements Plugin<Project> {
	private static final Logger log = Logging.getLogger( HibernateBuildPlugin.class );

	@Override
	public void apply(Project project) {
		project.getGradle().getSharedServices().registerIfAbsent(
				"db",
				DatabaseService.class,
				spec -> {
					spec.getMaxParallelUsages().set( 1 );
				}
		);
	}
}

And a simple build service:

public abstract class DatabaseService implements BuildService<BuildServiceParameters.None> {
}

In the main build, I register the plugin and custom task like the following:

abstract class DbTestTask extends Test {
	@Internal
	abstract Property<org.hibernate.build.DatabaseService> getDatabaseService();

	@Override
	@TaskAction
	void executeTests() {
		getDatabaseService().get()
		super.executeTests()
	}
}

apply from: rootProject.file( 'gradle/libraries.gradle' )
apply from: rootProject.file( 'gradle/databases.gradle' )

apply plugin: 'org.hibernate.build.plugin'
if ( !project.plugins.hasPlugin( 'java' ) ) {
	apply plugin: 'java'
	project.tasks.replace( "test", DbTestTask.class )
}

Can anyone help me understand what I did wrong here? Here is the project: GitHub - beikov/hibernate-orm at parallel-build

I’d love to hear about this also. The docs on shared build services imply that this should work, but it does not in my experience either. To access such a service, my tasks always have to look it up

The missing part here is that you need to set the registered build service to the Property on the task as shown in this example:

def dbService = project.getGradle().getSharedServices().registerIfAbsent(
				"db",
				DatabaseService.class,
				spec -> {
					spec.getMaxParallelUsages().set( 1 );
				}
		);

tasks.withType(DbTestTask).configureEach {
   databaseService.set(dbService)
}

So there is no way to simply have custom services injected?

The documentation does not mention that this is required: Shared Build Services

I was of the impression that this happens automatically. It also seems that the maxParallelUsages does not work properly. I still see test tasks being executed in parallel. Is this supposed to work?

I agree that this could be better documented.
In the section Registering a build service, example 3 shows the download task’s server property being set to the registered provider and there’s a small blurb after the example:

The plugin registers the service and receives a Provider<WebService> back. This provider can be connected to task properties to pass the service to the task.

It’s very easy to miss these subtle details and probably needs some improvement.

Currently there is not. IIUC, the build service class and the parameter object determine a build service uniquely. This means you would need to qualify the build service with its parameters in the task, which is currently not possible.

Looks like there is an issue for injectable build services: Injectable Shared Build Services · Issue #16168 · gradle/gradle · GitHub

Great to know, thanks. Do you have an example somewhere that shows how the maxParallelUsages is supposed to work exactly? It’s not having any effect in our experiments. How is a “usage” registered/unregistered?

I’m able to reproduce what you’re experiencing. The only way I can get it to work correctly is if I use the DSL method Task.usesService.

abstract class MyService implements BuildService<org.gradle.api.services.BuildServiceParameters.None> {
    void hello() {
        println 'hello'
    }
    void goodbye() {
        println 'goodbye'
    }
}
abstract class MyTask extends DefaultTask {
    @Internal
    abstract Property<MyService> getMyService()

    @TaskAction
    void doWork() {
        myService.get().hello()
        Thread.sleep( 5000 )
        myService.get().goodbye()
    }
}
subprojects {
    Provider<MyService> theService = gradle.sharedServices.registerIfAbsent( "myService", MyService ) {
        maxParallelUsages = 1
    }

    tasks.register( 'myTask', MyTask ) {
        myService = theService
        usesService( theService )
    }
}

Maybe I missed some tests, but it looks like all the integration tests around this capability use the DSL and there’s no tests using annotated properties.

Side note; org.gradle.api.services.BuildServiceParameters should probably be added to the automatic imports like org.gradle.api.services.BuildService is.

Thanks, the usesService part was what I was missing. It would be great if the documentation were showing that this is necessary.