Home » Java » java – Spring JPA: how to make sure a new transaction starts from the committed state of a previously committed transaction?-Exceptionshub

java – Spring JPA: how to make sure a new transaction starts from the committed state of a previously committed transaction?-Exceptionshub

Posted by: admin February 25, 2020 Leave a comment

Questions:

In a Spring boot with data JPA project with Hibernate on a PostgreSQL database, multiple tasks are executing simultaneously. There’s a TaskExecutor pool and a database connection pool.

Sometimes these tasks require some of the same objects (update: with object we mean objects stored in the database). In an attempt to ensure the tasks don’t conflict (update: “don’t try to access/modify the same records at the same time”), a locking service was created. A task gets a lock on the objects it requires and only releases the lock when the task is done, at which time the next task can get a lock on them and start its work.

In practice, this isn’t working. One particular case of a record being deleted in task A and still being visible during part of task B keeps popping up. The actual exception is a foreign key constraint not being fulfilled: task B first selects the (deleted) object as one for which a relationship is to be created (so task B still sees the deleted object at this point!), but then upon creation of the relationship in task B it fails because the deleted object is no longer present.

After consultation with a colleague, the idea came up that flushing a repository isn’t quite the same as committing. Hence, task A unlocks when its logic is done and changes are flushed, but the changed data has not yet been actually committed to the database. In the mean time, task B gets a lock and starts reading the data and only a little bit later the commit for task A happens.

To make sure the lock of task A is only released after its database changes have been committed, I tried this code:

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
    @Override
    public void afterCompletion(int status) {

        logDebugMessage("after completion method called");

        // Release the locks
        if(lockObtained) {
            logDebugMessage("releasing locks");
            lockService.releaseLocks(taskHolder.getId());
            logDebugMessage("done releasing locks");
        }
        else
            logDebugMessage("no locks to release");
    }
});

That, in itself, didn’t make the issue disappear. Next brainwave was that the next task, task B, already has a transaction open while it’s waiting for a lock. When it gets a lock, it reads using this already-open transaction and then ?for some reason? reads data from before the commit. I admit this doesn’t make much sense, but some desperation is beginning to set in. Anyway, with this additional idea, every task is now run as such:

@Override
public void run() {

    try{
        // Start the progress
        taskStartedDateTime = LocalDateTime.now();
        logDebugMessage("started");

        // Let the task determine which objects need to be locked
        List<LockableObjectId> objectsToLock = getObjectsToLock();

        // Try to obtain a lock
        lockObtained = locksObtained(objectsToLock);
        if(lockObtained) {

            // Do the actual task in a new transaction
            TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
            transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {

                    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                        @Override
                        public void afterCompletion(int status) {

                            logDebugMessage("after completion method called");

                            // Release the locks
                            if(lockObtained) {
                                logDebugMessage("releasing locks");
                                lockService.releaseLocks(taskHolder.getId());
                                logDebugMessage("done releasing locks");
                            }
                            else
                                logDebugMessage("no locks to release");
                        }
                    });

                    try{

                        // Run the actual task
                        runTask();

But this still doesn’t resolve the issue.
Is it possible the commit to the database was done from Java side and task B reads the database before the commit is done in the database itself? Does the afterCompletion method get called after Java sent the commit, but before the database has actually executed it? If so, is there a way to get a database confirmation that the commit has actually been executed?

Or are we entirely on the wrong track here?

How to&Answers: