Skip to content

Flaky test: UserRepositoryEntityGraphTest bounded-query-count assertion reads a SessionFactory-global counter under parallel execution #337

Description

@devondragon

Summary

UserRepositoryEntityGraphTest.shouldLoadRolesAndPrivilegesInBoundedQueryCountViaEntityGraphFinder is flaky. It intermittently fails with a prepared-statement count higher than the asserted bound. It is not a real regression — the query behavior is correct; the measurement is not isolated under JUnit parallel execution.

Observed failure

CI run 29130212931, Test on Java 25 (runtime compatibility) leg, on a docs-only commit (b789209):

UserRepositoryEntityGraphTest > shouldLoadRolesAndPrivilegesInBoundedQueryCountViaEntityGraphFinder FAILED
  java.lang.AssertionError: [EntityGraph finder should load roles + privileges in a bounded number of queries]
  Expecting actual:
    7L
  to be less than or equal to:
    3L
    at UserRepositoryEntityGraphTest.java:139

The commit was a changelog edit that cannot affect SQL, and the commits immediately before and after it passed all matrix legs — confirming flakiness rather than regression.

Root cause

The test reads a SessionFactory-global Hibernate Statistics counter:

// UserRepositoryEntityGraphTest.java:65-70
private Statistics statistics() {
    EntityManager em = entityManager.getEntityManager();
    Statistics stats = em.getEntityManagerFactory().unwrap(SessionFactory.class).getStatistics();
    stats.setStatisticsEnabled(true);
    return stats;
}

// lines 124-139
Statistics stats = statistics();
stats.clear();                       // clears the GLOBAL counter
User user = userRepository.findWithRolesByEmail("bounded@test.com");
...
assertThat(stats.getPrepareStatementCount()).isLessThanOrEqualTo(3);

getPrepareStatementCount() is scoped to the whole persistence unit, not the current session/thread. The suite runs with JUnit 5 parallel execution, and @DataJpaTest classes share the same cached Spring context (and thus the same SessionFactory and Statistics object). Concurrently-running tests increment the shared counter between stats.clear() and the assertion read, so the observed count (7) exceeds the real per-query count (≤3). The counter is not transactional, so @DataJpaTest rollback does not isolate it.

Suggested fix

Make the statement count isolated per-test rather than reading a global SessionFactory statistic. Preferred approach: count statements via a thread-local StatementInspector (each parallel test runs on its own thread/JDBC connection, so a thread-local counter is naturally isolated), then assert on that count. This preserves the N+1 regression-guard value of the test without the parallel-execution pollution.

Alternatives considered (less good): pinning the class to same-thread execution does not help because other classes on other threads still mutate the shared counter; removing the count assertion loses the N+1 guard.

Notes

  • Introduced by 70e1f99 perf: lazy role/privilege fetch with EntityGraph on the auth path (regression guard for the User.roles EAGER → LAZY switch).
  • Low urgency: main is currently green; this only causes intermittent red builds.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingjavaPull requests that update java code

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions