BuildService Singleton

I’ve been experimenting with using build services in my Gradle monorepo. I have a few questions for which I struggled to find satisfactory answers:

If I call project.getGradle().getSharedServices().registerIfAbsent from a plugin that is applied to multiple projects, will I get a single BuildService instance or will I get one for each project? I added a static AtomicInteger to the constructor for my BuildService and it indicates that I’m getting multiple instances of the same build service over the course of the build. Am I getting one per project? If so, how is this happening? getSharedServices exists on the Gradle instance and not on the project, so how does it know the context? I also tried experimenting with calling project.getRootProject().getGradle()... but that didn’t seem to change behavior either. Alternatively, is there one global build service each time it’s created, but Gradle is calling close on it once no one is using it?

How do I get a global BuildService singleton for the lifetime of my build? Is there a better pattern to use here?

Thanks!

The lifecycle of a shared build service indeed is the complete build run, you cannot give it a more narrow scope currently.

If you register the build service from the different projects with different names, you get separate ones. But if you need the same name, you should get the same build service.

What could be problematic is, if you have rootproject with subproject A and subproject B and apply the plugin in A and B. Because that way the plugin in A and B are on different classloaders and thus are different classes and thus different plugins. Try putting it with apply false to the root project, or as dependency in buildSrc or similar, so that A and B are taking the plugin from the same classloader.

Static state is pretty bad though, in a shared build service as in any other build logic, because static would outlive a build execution and be reused on other build invocations, potentially even of other projects within the same Gradle daemon. So never use static state.

1 Like

Thanks for the quick response. I think that explains most of the behavior I was seeing. I had a private static final AtomicInteger variable that was incrementing every time the build service constructor is called. If I run successive builds, it increased by 1 with each build. However, if I run gradle --stop followed by the build command, it never goes above 1.

So it seems like I was only getting one instance of my build service per build, but the class was remaining in the daemon’s classloader and being re-used from one run to the next. Neat.

Question: suppose I want something akin to a build service, but I want just one per project. How do I do that? Are project extensions the best way to implement this?

No, extension is for your consumers to configure or get something.

I already gave you the solution actually. Include the project path in the service name, for example as suffix.

That makes sense. I can set that up.

Suppose this per-project build service requires config information from an extension on the project, what’s the best way to pass that in? Does that need to come in on the build service parameters or can I inject it?

Should I just pull each Property off the extension that I require and put those on the build service’s parameters? That seems like it’s potentially the most configuration cacheable friendly route to go, no?

I’ve set up my build service to use a parameters object which holds all of the properties it requires from the extension. This is working nicely so far.

However, I now have two build services. MyProjectBuildService exists on every project which uses my plugin, PluginA. MyRootProjectBuildService is the singleton with a static name that only exists once on a build and comes from PluginB.

I need a reference to MyRootProjectBuildService inside of MyProjectBuildService. I set up MyRootProjectBuildService for injection into MyProjectBuildService using @ServiceReference, but it seems like that’s not an option (I’m getting an error about it). Am I correct that I can’t inject one build service into a second build service? What’s the proper pattern here?

Yes, I think wiring the extension properties to the build service parameters would be the way to go if you need values from the extension in the service.

What’s the proper pattern here?

I never tried that.
No idea whether something like that can work.