-
Notifications
You must be signed in to change notification settings - Fork 38.7k
Description
Description
A deadlock occurs during the Spring application context initialization when a DataSource bean creation blocks on a remote HTTP call using WebClient, and that call is observed by Micrometer.
The root cause appears to be a circular dependency and a shared resource lock. The main thread, which is responsible for context initialization, gets blocked at WebClient.block() while waiting for the credentials. The reactor-http thread, handling the reactive web call, in turn attempts to acquire a lock on the JpaMappingContext bean, which is already held by the main thread. This leads to a deadlock, and the application hangs indefinitely during startup.
This issue is likely to occur in scenarios where a bean required for JPA's initialization (like the DataSource in this case) depends on a synchronous call to a remote service that itself is instrumented by Micrometer's ObservationInterceptor.
Steps to Reproduce
- Create a Spring Boot application using Spring Data JPA, WebClient, and Micrometer.
- Configure a DataSource bean that fetches its credentials from a remote HTTP service using WebClient.block().
- Ensure the WebClient is configured to use ObservationInterceptor (standard with Micrometer auto-configuration).
- Run the application.
We faced this issue with Spring Boot 3.3 (Spring Framework 6.1).
With Spring Boot 3.4 and Spring Framework 6.2.2 it's not reproduced. #34247 8771b9e
But following this issue #34902 fa168ca, with Spring Framework 6.2.7 there is deadlock.
Code to Reproduce
download this zip and add wiremock and java code
Spring Boot App
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>deadlock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>deadlock</name>
<description>deadlock</description>
<url/>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock.integrations</groupId>
<artifactId>wiremock-spring-boot</artifactId>
<version>3.10.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@SpringBootApplication
public class DeadlockApplication {
public static void main(String[] args) {
SpringApplication.run(DeadlockApplication.class, args);
}
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
@Component
public class DbCredentialsProvider {
private final WebClient webClient;
public DbCredentialsProvider(@Value("${service.base-url:http://localhost:8081}") String url, WebClient.Builder builder) {
this.webClient = builder.baseUrl(url).build();
}
public Creds getCredentials() {
return webClient.get().uri("/db-credentials")
.retrieve()
.bodyToMono(Creds.class)
.block();
}
public record Creds(String username, String password) { }
}
package com.example.deadlock;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.sql.DataSource;
@Configuration
@DependsOn("wireMockServer") // need to run TestDeadlockApplication
public class DbConfig {
@Bean
public DataSource dataSource(DbCredentialsProvider credentialsProvider) {
// Uses credentials from provider (which triggers WebClient call)
Creds credentials = credentialsProvider.getCredentials();
return DataSourceBuilder.create()
.url("jdbc:h2:mem:testdb")
.username(credentials.username())
.password(credentials.password())
.build();
}
}
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Test {
@Id
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
import org.springframework.data.jpa.repository.JpaRepository;
public interface TestRepository extends JpaRepository<Test, String> {
}
//Run this code
public class TestDeadlockApplication {
public static void main(String[] args) {
SpringApplication.from(DeadlockApplication::main).with(WireMockServerConfig.class).run(args);
}
}
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WireMockServerConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public WireMockServer wireMockServer() {
WireMockConfiguration config = WireMockConfiguration.options()
.port(8081)
.usingFilesUnderDirectory("src/test/resources");
return new WireMockServer(config);
}
}
src/test/resources/mapping
db-credentials-mapping.json
{
"request": {
"method": "GET",
"url": "/db-credentials"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"username\":\"testuser\",\"password\":\"testpass\"}"
}
}
Debugger Screenshot Analysis

Expected Behavior
The application should start successfully, with the DataSource bean being created after the DbCredentialsProvider has completed its HTTP call.
Actual Behavior
The application hangs during startup and never initializes due to a deadlock. Screenshot show the main thread blocked on the WebClient.block() call, and the reactor-http thread blocked while trying to acquire a lock on the JpaMappingContext bean.