Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propagation REQUIRES_NEW may cause connection pool deadlock #26250

Closed
wlizhi opened this issue Dec 10, 2020 · 28 comments
Closed

Propagation REQUIRES_NEW may cause connection pool deadlock #26250

wlizhi opened this issue Dec 10, 2020 · 28 comments
Assignees
Labels
in: data Issues in data modules (jdbc, orm, oxm, tx) status: backported An issue that has been backported to maintenance branches type: documentation A documentation task
Milestone

Comments

@wlizhi
Copy link

wlizhi commented Dec 10, 2020

A method that opens a transaction calls another method that starts a new transaction,if all connections are exhausted before the last new transaction method is executed,then all threads in the process will block,this process will fail.

Pseudo code:

@Transactional
methodA(){
    // All the threads that started the transaction were executed here, but the connection was exhausted.
    // The latter method execution will get a new connection,but it will never get it.
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    methodB(){
    }
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 10, 2020
@quaff
Copy link
Contributor

quaff commented Dec 10, 2020

What's the problem?

@wlizhi
Copy link
Author

wlizhi commented Dec 10, 2020

Propagation.REQUIRES_NEW May cause all connections to be occupied.if this happens, the entire process will be blocked and unable to provide services

@wlizhi
Copy link
Author

wlizhi commented Dec 10, 2020

When all connections in the connection pool are occupied by the transaction method that does not execute to the last newly started transaction, all threads cannot obtain new connections, and the whole process will be blocked.

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

Do you know REQUIRES_NEW will double connections?

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

REQUIRES_NEW is not_ New will occupy two connections, but this transaction propagation behavior will suspend the previous transaction and restart a transaction. When a new transaction is opened, a new connection must be obtained. The previously suspended transaction connection will not be released.

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

It's by design, If first connection released, the outer transaction cannot continue.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
if (debugEnabled) {
logger.debug("Suspending current transaction, creating new transaction with name [" +
definition.getName() + "]");
}
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
return startTransaction(definition, transaction, debugEnabled, suspendedResources);
}
catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
}

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

The above is the source code. When a new transaction is opened, the previous transaction will be suspended, so that the peripheral methods will occupy multiple connections during the execution process. Suppose that there are 50 connections in the database connection pool. At this time, 50 threads are just executing to the peripheral methods. They are all preparing to open new transactions and obtain new connections, but they will never get them because the connections have been exhausted.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

After this happens, all connections in the process may be blocked.When this happens, all threads that will start the transaction will be blocked.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

My question is, this way is likely to cause the whole process to block, so what is the significance of it?

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

I know that if the connection has been released, the external transaction cannot be resumed. But REQUIRES_NEW may cause the whole process to hang up. So what's the point of it?

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

I explained why REQUIRES_NEW will double connections, If your connection pool is exhausted, you should increase max pool size, or handle the concurrency, for example by using Bulkhead of resilience4j, it's not responsibility of spring transaction.
I didn't get your point, do you think there is a bug or improvement here? If you want ask question, please visit stackoverflow.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

@service
public class PropagationExampleServiceImpl implements PropagationExampleService {
@Autowired
PropagationExampleService propagationExampleService;

@Transactional
@Override
public void methodA() throws InterruptedException {
	Thread.sleep(100);
            // Assuming that multiple threads are executed here, all connections in the connection pool are used to open 
           // transactions of peripheral methods. Then they will never be released.
	propagationExampleService.methodB();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void methodB() throws InterruptedException {
	Thread.sleep(100);
}

}

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

Nested new start transactions cannot be used in actual production environments. It is likely that all connections are occupied when executing to the peripheral transaction method, and then blocked at the method of the newly started inner transaction.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

I think there is something wrong with the design of REQUIRES_NEW.

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

It's not forum here, if you think it's a bug please paste test case, if you think it's an improvement please submit PR, end of discussion.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

Sorry, I didn't make it clear. This is the test code, where it blocks the real process. Methodb() will never execute.

@slf4j
@service
public class PropagationExampleServiceImpl implements PropagationExampleService {
@Autowired
PropagationExampleService service;

@Transactional
@Override
public void methodA() throws InterruptedException {
	Thread.sleep(100);
	log.info("methodA");
	service.methodB();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void methodB() throws InterruptedException {
	log.info("methodB");
	Thread.sleep(100);
}

}

@SpringBootTest
public class PropagationExampleServiceTest {
@Autowired
PropagationExampleService service;
ExecutorService executor = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));

@Test
public void methodATest() {
	for (int i = 0; i < 20; i++) {
		executor.execute(() -> {
			try {
				service.methodA();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
	}
	LockSupport.park();
}

}

spring:
datasource:
name: druidDataSource
type: com.alibaba.druid.pool.DruidDataSource
druid:
max-active: 10

Here is the log:
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-1] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-2] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-5] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-6] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-8] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-9] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-3] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [ool-1-thread-10] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-7] c.j.s.s.i.PropagationExampleServiceImpl : methodA
2020-12-11 10:15:56.385 INFO 10864 --- [pool-1-thread-4] c.j.s.s.i.PropagationExampleServiceImpl : methodA

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

I think the result is expected, you should increase max-active > concurrency, so If you change max-active to 21, it will pass, waiting for your confirmation.
You should set a max-wait on dataSource, then getConnection will timeout instead of endless blocking.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

I know, I just want to express that. In fact, the number of connections in the connection pool is generally not large. In the above example, only one layer is nested. If there are multiple layers of newly started transactions in the code, only a small amount of concurrency is needed to trigger the above result.

I just think that when multiple threads compete for resources, each thread must grab multiple resources before ending the task, which may be a hidden danger.

Because this method may cause multiple threads to rob only a part of the resources they need. They can't release what they have acquired without getting all the resources they need. By this time, however, resources have been exhausted.

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

Your test blocked because you didn't set timeout for ds.getConnection(), for example maxWait for druid and connectionTimeout for HikariCP.
It's really nothing to do with spring transaction.

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

I'm sorry to have taken you so long.

I just want to put forward what I think may be hidden dangers. It's not that I have encountered problems in use. I just don't quite understand why this place is designed in this way.

Even if Max wait is set here, the above situation may occur. It's just going to wait for a while, not the whole process, but it's bad enough.

For this reason, I choose to avoid using REQUIRES_NEW, if I need to start a new transaction in a method, I will use asynchronous or transactionSynchronization.afterCommit ()。

You may say that the outer method will not be able to sense the exception, but why should it? These are two transactions. If I need the outer layer to sense the inner layer's anomaly, I can use NESTED.

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

I admit your title "Propagation.REQUIRES_NEW May cause all connections to be occupied".
application should deal REQUIRES_NEW properly, there is no bug or improve space here, at most adding note in docs. Do you agree?

@wlizhi
Copy link
Author

wlizhi commented Dec 11, 2020

Thank you for taking the time to listen to me describe the problem,That's what I mean. If we must use 'REQUIRES_NEW' needs to be dealt with properly. It would be great if it could be mentioned in the documentation to make other developers aware of it.

@quaff
Copy link
Contributor

quaff commented Dec 11, 2020

Thank you for taking the time to listen to me describe the problem,That's what I mean. If we must use 'REQUIRES_NEW' needs to be dealt with properly. It would be great if it could be mentioned in the documentation to make other developers aware of it.

The javadoc and reference should mention that REQUIRES_NEW will retain double resources(such as JDBC connections) if a current transaction already exists, WDYT @jhoeller ?

@wlizhi

This comment was marked as resolved.

@quaff

This comment was marked as resolved.

@rstoyanchev rstoyanchev added the in: data Issues in data modules (jdbc, orm, oxm, tx) label Nov 10, 2021
@roma2341
Copy link

roma2341 commented Feb 2, 2022

Requires_new is king of side effects. When i began to wrote integration tests i understood that this propagation prevents my transactions from rollback, but i didnt expect this behaviour would make my database dirty after a test, i had transactional annotation on my test methods and i know that requires_new starts new transaction so it wouldn't be rolled-back, only outer transaction would be rolled-back. And i don't know how to avoid this behaviour in my tests, only way is to not to use requires_new. And what is weird i have never heard about this problem when read articles about testing spring applications. And why i also dont like requires_new is that it can make my program throw pessimistic lock exceptions and it's very hard to find a reason

@jhoeller jhoeller added type: documentation A documentation task and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Aug 8, 2023
@jhoeller jhoeller self-assigned this Aug 8, 2023
@jhoeller jhoeller added this to the 6.0.12 milestone Aug 8, 2023
@jhoeller jhoeller changed the title Propagation.REQUIRES_NEW May cause all connections to be occupied Propagation REQUIRES_NEW may cause connection pool deadlock Aug 12, 2023
@jhoeller jhoeller added the for: backport-to-5.3.x Marks an issue as a candidate for backport to 5.3.x label Aug 12, 2023
@github-actions github-actions bot added status: backported An issue that has been backported to maintenance branches and removed for: backport-to-5.3.x Marks an issue as a candidate for backport to 5.3.x labels Aug 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: data Issues in data modules (jdbc, orm, oxm, tx) status: backported An issue that has been backported to maintenance branches type: documentation A documentation task
Projects
None yet
Development

No branches or pull requests

6 participants