17

I have a somewhat complicated Spring Boot app, with a large number of tests.

When running the tests, it seems to be accumulating a lot of threads, one of which there is multiple instances of and is called SimplePauseDetectorThread_0, which I traced down to this dependency

|    |    |    \--- io.micrometer:micrometer-core:1.1.1
|    |    |         +--- org.latencyutils:LatencyUtils:2.0.3

This seems to happen on Spring Boot 2.0.6 as well as 2.1.1.

A typical test may look like this:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@ActiveProfiles(profiles = {"test"})
public class MyTest {
[...]

My actuator config looks like this:

management.endpoints.enabled-by-default=false
management.endpoint.prometheus.enabled=true
management.endpoints.web.base-path=/
management.endpoints.web.exposure.include=prometheus
management.endpoints.web.path-mapping.prometheus=prometheus
spring.metrics.prometheus.enabled=true

See attached screenshot

enter image description here

Erik Živković
  • 4,867
  • 2
  • 35
  • 53

1 Answers1

29

Snicoll from Pivotal helped me on GitHub, by suggesting that it was probably connected to context caching in the spring boot test framework.

If you have a large number of tests that are using the spring integration and a somewhat important number of context configurations (you've only shown one class), then one context per configuration will be created and I can see the number of threads increasing in that scenario.

He then pointed me to the relevant documentation, which states:

You can configure the maximum size from the command line or a build script by setting a JVM system property named spring.test.context.cache.maxSize. As an alternative, you can set the same property programmatically by using the SpringProperties API.

And org.springframework.core.SpringProperties states:

Reads a {@code spring.properties} file from the root of the Spring library classpath

Which leaves us with two ways of setting maxSize.

Option 1. Configure the gradle test task

Add a property to the gradle test task which will configure the GradleWorkerMain, in build.gradle:

test {
    jvmArgs "-Dspring.test.context.cache.maxSize=1"
}

If you have many subprojects you might want to use this option.

See Bonus below for a way to apply the setting to all your subprojects.

Option 2. Add a spring.properties to your test resources

You can write the setting in my-service/src/test/resources/spring.properties, like so:

spring.test.context.cache.maxSize=1

Conclusion

Now my tests are running nicely with less memory consumption, and fewer threads.

Bonus

This also resolves the issue with Gradle 5+ having workers that have 512MB max heap by default (instead of 25% of system RAM) - the subproject test suites no longer blow away all the available RAM which would cause the workers to OOM if I did not add a custom jvmargs with a larger heap in the test configuration of java projects. I can now run with "vanilla" heap size in the gradle worker.

If one does want to tweak the RAM available to Gradle tests, do something like this in the root build.gradle:

allprojects { project ->
    project.plugins.withId('java') {
        test {
            maxHeapSize = "1536M"
            // If you don't want to use spring.properties (or add other JVM args)
            jvmArgs "-Dspring.test.context.cache.maxSize=1"
        }
    }
    project.plugins.withId('java-library') {
        test {
            maxHeapSize = "1536M"
            // If you don't want to use spring.properties (or add other JVM args)
            jvmArgs "-Dspring.test.context.cache.maxSize=1"
        }
    }
}
Erik Živković
  • 4,867
  • 2
  • 35
  • 53
  • 1
    You can also use the `properties` member of `@SpringBootTest` to inject custom properties. Handy if you want to disable various Spring features you do not need during tests (or enable, or simple custom configuration, of course). – user268396 Jan 09 '19 at 22:17
  • There is one drawback with this approach and that is that now instead of using a cached version of the context it will need to create a new one. This might lead to longer test periods. So while you might gain some memory it might lead to other issues as well. – M. Deinum Jan 10 '19 at 06:51
  • @M.Deinum I have seen a decrease in test times, since I have multiple projects with multiple integration tests I can now run them in parallel with gradle. So this is a huge win for me. – Erik Živković Jan 10 '19 at 08:22
  • We also run them in parallel with context caching in gradle (with a fairly large Spring Boot app). You might want to create test suites which use the same context and run those suites in parallel that way you benefit from the caching and prevent the need to reload/restart your whole application. In our case starting the application will take over 1 minute which you can image will severely impact test performance. – M. Deinum Jan 10 '19 at 08:24
  • @M.Deinum We have a test strategy that uses a real MySQL database, each test is run in its own schema to avoid failures and so that custom property creates a new context every time. If you have a way to solve that and have cache reuse I'm all ears. – Erik Živković Jan 10 '19 at 08:37
  • 1
    Maybe time to introduce a test scope... Which could proxy and create a `DataSource` per test, that would, probably, be faster then re-creating the full context. – M. Deinum Jan 10 '19 at 08:43
  • 1
    @ErikZivkovic it probably didn't work because you set a system property to the Gradle VM which is not passed to the forked process running your tests. Can you double check your setup to make sure you're passing the system property to the right process? – Stephane Nicoll Jan 10 '19 at 09:34
  • @StephaneNicoll that is correct. I realized later that you need to send those JVM args using the test configuration for java and java-library. I will update my answer with that information once I figure out how to apply the advice from M. Deinum – Erik Živković Jan 10 '19 at 21:36
  • @M.Deinum I tried to set up testing using a custom test scope, but quickly ran into trouble with circular references to DataSource. As I understand it (from this answer https://stackoverflow.com/a/10132914 ) spring can resolve some circular dependencies for singleton scoped beans. I guess I could manually start resolving that stuff but I don't have the time to go down that particular rabbit hole. I will try to solve my issues in some other way so that I can benefit from context caching. Thanks for your help so far, anyway! – Erik Živković Jan 12 '19 at 20:37
  • 1
    @StephaneNicoll I found a way to add maxSize to jvmArgs using the gradle test configuration. Restructured my answer a bit to reflect that fact and added information on how to do it. Thanks for pushing me towards finding out how to do it. – Erik Živković Jan 12 '19 at 21:09
  • @ErikZivkovic not sure why you would run into circular dependencies with adding a custom scope... The only thing would be that you add an `@TestScope` with an accompanying `TestScope` implementation (which implements `Scope`). This should store the actual instance in the `TestContext` using the `setAttribute` method. I don't see how this would lead to a circulair dependendency. – M. Deinum Jan 14 '19 at 07:19
  • @M.Deinum That's exactly what I did with some Baeldung and StackOverflow help. I ran in to circular dependencies with Flyway amongst other things. Anyway my tests are running really fast with maxSize=1 so I'm happy for now! – Erik Živković Jan 14 '19 at 08:56