spring-projects / spring-data-neo4j

Provide support to increase developer productivity in Java when using Neo4j. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
http://spring.io/projects/spring-data-neo4j
Apache License 2.0
822 stars 620 forks source link

Transaction timeout is not happening when setting the spring.transaction.default-timeout property. #2922

Closed amit-kumaryadav closed 1 month ago

amit-kumaryadav commented 1 month ago

When I'm using declarative method level @Transactional(timeout = 15), then transaction timeout is happening properly. But when I removed the timeout property from @Transactional and put spring.transaction.default-timeout=15 this time timeout is not happening.

michael-simons commented 1 month ago

Hello @amit-kumaryadav I cannot reproduce this.

Here's a test showing all combinations I would say we support:

package org.neo4j.timeoutdemo;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.time.Duration;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers(disabledWithoutDocker = true)
@Import(ServiceConnectionsConfig.class)
class TimeoutTests {

    static final String EXPECTED_MSG = "The transaction has been terminated. Retry your operation in a new transaction, and you should see a successful result. The transaction has not completed within the timeout specified at its start by the client. You may want to retry with a longer timeout.";

    @DynamicPropertySource
    static void setDefaultTime(DynamicPropertyRegistry registry) {
        System.out.println();
        registry.add("spring.transaction.default-timeout", () -> Duration.ofSeconds(3));
    }

    @Test
    void defaultTimeoutGetsApplied(@Autowired BookRepository bookRepository) {
        assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class)
            .isThrownBy(() -> bookRepository.save(new Book("Test")))
            .withMessageContaining(EXPECTED_MSG);
    }

    @Test
    void annotatedMethodTimeoutGetsApplied(@Autowired BookRepository bookRepository) {
        assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class)
            .isThrownBy(bookRepository::findAll)
            .withMessageContaining(EXPECTED_MSG);
    }

    @Test
    void usingTransactionTemplate(@Autowired BookRepository bookRepository, @Autowired Neo4jTransactionManager transactionManager) {

        var transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setTimeout(1); // Seconds, again
        assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class)
            .isThrownBy(() -> transactionTemplate.execute(
                tx -> bookRepository.findByTitle("whatever"))
            )
            .withMessageContaining(EXPECTED_MSG);
    }

    @Test
    void usingTransactionTemplateWithNeo4jTemplate(@Autowired Neo4jTemplate neo4jTemplate, @Autowired Neo4jTransactionManager transactionManager) {

        var transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setTimeout(1);
        assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class)
            .isThrownBy(() -> transactionTemplate.execute(
                tx -> neo4jTemplate.findAll("CALL apoc.util.sleep(2000) MATCH (n:Book) RETURN n", Book.class)))
            .withMessageContaining(EXPECTED_MSG);
    }
}

In general, the algorithm is such that if anything is specified on the method or on the transaction template with a value greater zero, that value applies. Otherwise, if the default value is greater zero, the default applies. If no value is specified, no time out is applied. I can only guess that you might have a declaration laying around somewhere.

Here's the full test project attached: timeoutdemo.zip

amit-kumaryadav commented 1 month ago

@michael-simons In in our project extensively we are using Neo4jClient instead or Repository, I tried with Neo4jClient and it's not working, I'm using spring boot 3.2.5 version.

@Transactional( value = "transactionManager", readOnly = true)
    public VehicleResponse getVehicleData(String id) {
        Optional<Map<String, Object>> queryResult =
                neo4jClient.getResultUsingNeo4jClient(QueryConstants.QUERY_TO_GET_VEHICLE_DATA, Map.of(Constants.ID, id));
        if (queryResult.isEmpty()) {
            return VehicleResponse.builder().build();
        }
        return commonUtils.convertValue(queryResult.get().get("result"),
                VehicleResponse.class);
    } 
michael-simons commented 1 month ago

Works for me, even with 3.2.5

See updated projects:

timeoutdemo_325.zip timeoutdemo_331.zip

My suspicion is that for some reason you reference the wrong tx-manager. I know that this is necessary in 3.2.5 (and no longer in 3.3.x).

spring-projects-issues commented 1 month ago

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

spring-projects-issues commented 1 month ago

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.