-
Notifications
You must be signed in to change notification settings - Fork 1.1k
DefaultLockRepository can misbehave when DB transaction is already active #3683
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
Comments
Hi @xak2000 ! I think all your investigations and conclusions are valid. I guess it is was designed in mind when this one is used outside of any end-user transactions. Would you mind to provide a PR with the fix you have in mind and we will continue a discussion over there (if any). Thank you! |
Hi @artembilan ! I think adding I'm not sure though how attributes of this annotation are merged when the annotation is present on both the class level and the method level. So, if the class has
and the method has
does it mean that the method will also effectively have So, the effective method-level annotation will be:
or
? |
If you are interested in that logic, then it is here in the
As you see there is no merging for those class-level and method-side annotations: if method has an annotation, it wins. Otherwise it is taken from the class-level if exists. I think you are right with your logic about class-level for this |
…w transaction If a transaction is already active while JdbcLockRegistry uses DefaultLockRepository to acquire/release a lock, the repository must execute SQL queries in a separate transaction to prevent problems with blocking, deadlocking etc. It also allows to properly follow transaction isolation level that is set on some methods of DefaultLockRepository. Previously all methods of DefaultLockRepository supported the current transaction if there are any. Now all methods will always create new transaction on each call.
…w transaction If a transaction is already active while JdbcLockRegistry uses DefaultLockRepository to acquire/release a lock, the repository must execute SQL queries in a separate transaction to prevent problems with blocking, deadlocking etc. It also allows to properly follow transaction isolation level that is set on some methods of DefaultLockRepository. Previously all methods of DefaultLockRepository supported the current transaction if there are any. Now all methods will always create new transaction on each call.
@artembilan Thank you for mentioning of I created a PR #3684. Please take a look when you have time. |
Fixes #3683 If a transaction is already active while `JdbcLockRegistry` uses `DefaultLockRepository` to acquire/release a lock, the repository must execute SQL queries in a separate transaction to prevent problems with blocking, deadlocking etc. It also allows to properly follow transaction isolation level that is set on some methods of `DefaultLockRepository`. Previously all methods of DefaultLockRepository supported the current transaction if there are any. Now all methods will always create new transaction on each call. * Change tests to be more unit-ish * Change `@since` for the `DefaultLockRepositoryTests` to `5.3.10` **Cherry-pick to `5.4.x` & `5.3.x`**
Fixes #3683 If a transaction is already active while `JdbcLockRegistry` uses `DefaultLockRepository` to acquire/release a lock, the repository must execute SQL queries in a separate transaction to prevent problems with blocking, deadlocking etc. It also allows to properly follow transaction isolation level that is set on some methods of `DefaultLockRepository`. Previously all methods of DefaultLockRepository supported the current transaction if there are any. Now all methods will always create new transaction on each call. * Change tests to be more unit-ish * Change `@since` for the `DefaultLockRepositoryTests` to `5.3.10` **Cherry-pick to `5.4.x` & `5.3.x`** # Conflicts: # spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java
Currently
acquire
method ofDefaultLockRepository
has@Transactional(isolation = Isolation.SERIALIZABLE)
annotation.spring-integration/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java
Line 162 in a843dae
But I wander what will be if I try to acquire a LOCK from the method that is already annotated with
@Transactional
?If the transaction is already started, the default propagation level is
REQUIRED
and that does mean that already opened transaction will be supported instead of creating a new one. And the documentation of@Transactional.isolation
attribute explicitly says:I'm not sure what does it mean in the context of
REQUIRED
propagation when it's an innerREQUIRED
transaction, but it looks like it will just do nothing with the isolation level if the transaction is already started. So the isolation level will be the same that is set by most outer transaction (real DB transaction that is already started).To me it looks like
@Transactional(isolation = Isolation.SERIALIZABLE)
annotation must includepropagation = Propagation.REQUIRES_NEW
attribute to make sure that LOCK acquiring will always be done in a separate transaction with SERIALIZABLE isolation level.I also see other potential problems with the shared transaction between user's code and LOCK repository. For example, any UPDATE of a record in
INT_LOCK
table will be blocked if other thread already acquired the lock and now executes the user's code under this LOCK (in the same transaction that acquired the lock). In this case an EXCLUSIVE DB LOCK on an INT_LOCK's record will be hold by a first transaction for the whole time while user's (outer) transaction is not committed or rolled back. So, e.g.JdbcLockRegistry.JdbcLock.tryLock
method will be blocked for a potentially long time.Another potential problem could be for the retry logic in the case of transient DB exception (e.g. deadlock).
Currently
JdbcLockRegistry.JdbcLock.lock()
method does retry is the case of one of:But this retry logic could not always work correctly if it will not start a new DB transaction before each retry. With current implementation of
acquire()
method it looks like it's not a problem, but if the method will e.g.SELECT
beforeUPDATE
in a REPEATABLE_READ transaction, the logic could be flawed because SELECT will always return the same result that was returned when it was executed the 1st time in this transaction. Anyway, it looks like the retry logic inJdbcLockRegistry.JdbcLock
was implemented with the assumption that a new transaction will be started by eachacquire
method execution.Another problem with a shared transaction I could imagine: a user's transaction that acquires and releases the LOCK and then executes a method with REQUIRES_NEW transaction that tries to acquire the same (already released) LOCK. It will not be able to do this because of a transaction opened by the caller method that is still active and just suspended. The java LOCK is already released by the caller's method, but the DB EXCLUSIVE LOCK on the deleted DB record of
INT_LOCK
table is still held by this suspended transaction, so new transaction in the called method could not UPDATE or INSERT the same record into the table:Also reusing the same transaction opened by user's code could increase the probability of deadlocks, as user's transaction could already hold some DB locks at the point of acquiring the LOCK, and if it does this in multiple places in reverse order, a deadlock will happen.
All in all, to me it looks like DefaultLockRepository and JdbcLockRegistry were designed with the assumption that a new transaction will be opened on each call to any repository method instead of supporting an already opened transaction. Am I right?
A also noticed the
@Transactional
annotation on the wholeDefaultLockRepository
class that looks strange to me as some methods likesetRegion
doesn't require a transaction. The annotation on the class is also applies to all methods of subclasses, so this makes extending ofDefaultLockRepository
class harder. This is not a major problem, though. Butpropagation = Propagation.REQUIRES_NEW
attribute also should be added here (or@Transactional
annotation should be moved from the class level to appropriate methods level).The text was updated successfully, but these errors were encountered: