Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@Async methods return empty lists. Why?

I'm trying to play around with @Async. My first experience with it isn't very positive. I get an empty list where results are definitely expected

MRE:

public interface AsynchronousCardRepository extends Repository<Card, UUID> {
    @Async
    Future<List<Card>> findAll();
}
@DataJpaTest
@ActiveProfiles("test")
@Sql(
        executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
        scripts = {"/schema-h2.sql", "/data-h2.sql"}
)
public class AsynchronousCardRepositoryTest {
    @Autowired
    AsynchronousCardRepository cardRepository;

    @Test
    @SneakyThrows
    void findAll() {
        Future<List<Card>> futureCards = cardRepository.findAll();
        List<Card> cards = futureCards.get();
        assertThat(cards).asList().hasSize(3);
    }
}
-- schema

CREATE TABLE IF NOT EXISTS PUBLIC.account
(
    id                        UUID           PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS PUBLIC.card
(
    id                UUID             PRIMARY KEY,
    account_id        UUID             NOT NULL REFERENCES account (id),
    status            VARCHAR(30)      NOT NULL
);
-- data

INSERT INTO PUBLIC.ACCOUNT
VALUES ('00000000-0000-0000-0000-000000000ac1');

INSERT INTO PUBLIC.CARD
VALUES ('00000000-0000-0000-0000-0000000000c1', '00000000-0000-0000-0000-000000000ac1', 'ACTIVE');

INSERT INTO PUBLIC.CARD
VALUES ('00000000-0000-0000-0000-0000000000c2', '00000000-0000-0000-0000-000000000ac1', 'ACTIVE');

INSERT INTO PUBLIC.CARD
VALUES ('00000000-0000-0000-0000-0000000000c3', '00000000-0000-0000-0000-000000000ac1', 'NON_ACTIVE');
@Entity
@Table(name = "card")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Card {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;
    @ManyToOne
    @JoinColumn(name = "account_id", referencedColumnName = "id")
    private Account account;
    @Enumerated(value = EnumType.STRING)
    private CardStatus status;
}
public enum CardStatus {
    ACTIVE, NON_ACTIVE
}
@Entity
@Table(name = "account")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private UUID id;
    @OneToMany(mappedBy = "account")
    private List<Card> cards;
}
@SpringBootApplication
@EnableAsync // funny enough, if you remove it, everything works
public class CreditServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(CreditServiceApplication.class, args);
    }
}
spring:
  datasource:
    url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true
    database-platform: org.hibernate.dialect.H2Dialect
    properties:
      hibernate:
        format_sql: true
  h2:
    console:
      enabled: false
      settings:
        web-allow-others: true
  config:
    activate:
      on-profile: test

logging:
  level:
    org:
      hibernate:
        type: TRACE
      springframework:
          jdbc:
            core: TRACE
            datasource:
              init: DEBUG
          test:
            context:
              jdbc: DEBUG

The method doesn't return anything:

java.lang.AssertionError: 
Expected size: 3 but was: 0 in:
[]

    at by.afinny.credit.unit.repository.AsynchronousCardRepositoryTest.findAll(AsynchronousCardRepositoryTest.java:32)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)

Why is it happening?

like image 515
Sergey Zolotarev Avatar asked Mar 05 '26 22:03

Sergey Zolotarev


2 Answers

While I don't know the exact cause, it seems to be related to how transactions are handled in @DataJpaTest. If you run the application regularly, the findAll() method works fine.

Similarly, if you use @SpringBootTest in stead of @DataJpaTest, your test also works:

@SpringBootTest // Replace @DataJpaTest with @SpringBootTest
@ActiveProfiles("test")
@Sql(
    executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
    scripts = {"/schema-h2.sql", "/data-h2.sql"}
)
public class AsynchronousCardRepositoryTest {
    // ...
}

The reason why I suspect that this is transaction related is because if I change the execution phase of the SQL to be BEFORE_TEST_CLASS, your test succeeds:

@DataJpaTest
@ActiveProfiles("test")
// Replace BEFORE_TEST_METHOD with BEFORE_TEST_CLASS
@Sql(
    executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS,
    scripts = {"/schema-h2.sql", "/data-h2.sql"}
)
public class AsynchronousCardRepositoryTest {
    // ...
}

Alternatively, if I annotate your test with @Transactional(propagation = Propagation.NEVER), your test also succeeds:

@DataJpaTest
@ActiveProfiles("test")
@Sql(
    executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
    scripts = {"/schema-h2.sql", "/data-h2.sql"}
)
@Transactional(propagation = Propagation.NEVER) // Add this
public class AsynchronousCardRepositoryTest {
    // ...
}
like image 60
g00glen00b Avatar answered Mar 07 '26 11:03

g00glen00b


The cause of the test failure is due to a combination of:

  1. The @DataJpaTest annotation itself is annotated with @Transactional, this mean the test will run inside a transaction, also indicated by the Javadoc in DataJpaTest:

    By default, tests annotated with @DataJpaTest are transactional and roll back at the end of each test.

  2. Your test is using @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, ...), so the SQL scripts will run within the test's transaction, therefore any data created by the script is uncommitted data while the test is running.
  3. Your async findAll() will run in a different thread, outside of the test's transaction, and due to transaction isolation (default is read committed), it will not see uncommitted data created in the test, hence it will return nothing.

Any one of the following can workaround this (mostly copied from the answer by @g00glen00b) and why they work:

  • Use @SpringBootTest instead of @DataJpaTest - this works because @SpringBootTest itself is not annotated with @Transactional, so there is no outer transaction wrapping the test, everything is committed as soon as their calls finish, so findAll() will see the committed test data.
  • Use @Transactional(propagation = Propagation.NEVER) or Propagation.NOT_SUPPORTED on the test, this essentially suspends the test transaction coming from @DataJpaTest, so will cause the test to run without a wrapping transaction, having similar affect as the above.
  • Use @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS, ...) - this runs and commits the test data before the test starts, therefore not affected by the test's own transaction.
  • Remove @Async on findAll() - this will cause findAll() to run within the same transaction, therefore will see uncommitted data.

Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!