Analyzing and Resolving Circular Dependency‑Induced SocketTimeoutException in Spring Cloud Microservices
The article investigates a recurring SocketTimeoutException caused by a circular dependency between two Spring Cloud services, explains the deadlock mechanism, demonstrates how removing the circular call resolves the issue, and provides verification steps with code, JMeter load testing, and thread‑dump analysis.
The author encountered frequent java.net.SocketTimeoutException: Read timed out errors in a test environment after deploying a new iteration, where a client called Foo.hello() , which invoked Boo.boo() , and Boo.boo() called back to Foo.another() , forming a circular dependency.
Although the calls were simple queries with minimal data, the circular invocation caused all threads in the Foo service's Tomcat pool to wait on Boo, while Boo simultaneously waited on Foo, leading to a deadlock‑like situation and eventual timeouts when request rate exceeded processing capacity.
To confirm the hypothesis, the author removed the circular call, after which the timeout disappeared, prompting a deeper analysis to ensure the fix addressed the root cause.
Verification was performed by building a minimal Spring Boot setup:
spring.application.name=demo-foo server.port=8000 eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka server.tomcat.threads.max=16
Foo service (using Feign to call Boo):
package com.cd.demofoo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class FooController { @Autowired BooFeignClient booFeignClient; @RequestMapping("/hello") public String hello(){ long start = System.currentTimeMillis(); System.out.println("[" + Thread.currentThread() + "] foo:hello called, call boo:boo now"); booFeignClient.boo(); System.out.println("[" + Thread.currentThread() + "] foo:hello called, call boo:boo, total cost:" + (System.currentTimeMillis() - start)); return "hello world"; } @RequestMapping("/another") public String another(){ long start = System.currentTimeMillis(); try { // simulate a slow call Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("foo:another called, total cost:" + (System.currentTimeMillis() - start)); return "another"; } }
Boo service (calling Foo):
package com.cd.demoboo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BooController { @Autowired FooFeignClient fooFeignClient; @RequestMapping("/boo") public String boo(){ long start = System.currentTimeMillis(); fooFeignClient.another(); System.out.println("boo:boo called, call foo:another, total cost:" + (System.currentTimeMillis() - start)); return "boo"; } }
Using JMeter with 30 concurrent threads in an infinite loop, the Foo service quickly became saturated, and Boo began logging the same timeout exceptions, confirming the deadlock scenario. A thread dump (jstack) showed all Foo threads stuck in the hello() method.
The conclusion emphasizes that circular dependencies between microservices act like class‑level cyclic dependencies, leading to severe coupling, potential deadlocks, and effectively turning a microservice architecture into a distributed monolith, which should be avoided.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.