Class AnalysisResultSetCacheStalenessTest
AnalysisResultSet + DifferentialExpressionAnalysisResult
HBM mapping has the same mutable="false" parent + child collection +
<cache usage="read-only"/> shape that caused the AuditTrail /
AuditEvent stale-empty-bag bug (fixed in ab8b4c443c).
The preemptive fix landed in 02c87a91ed: the L2 read-only cache
directives were dropped from both AnalysisResultSet.hbm.xml (on the
ExpressionAnalysisResultSet subclass and the hitListSizes
bag) and DifferentialExpressionAnalysisResult.hbm.xml (entity
cache + parent-class default), while mutable="false" was retained
because the rows themselves remain write-once-immutable.
This test exercises the cross-session reload path that surfaces the
symptom: write the parent ExpressionAnalysisResultSet (with N
results) in T1, flush+clear the session to drop L1 (simulating a fresh
Spring-managed session in a separate request / @Transactional boundary),
write a new DifferentialExpressionAnalysisResult to that result set
in T2, flush+clear again, then reload the result set in T3 and assert that
getResults() reflects the post-T1 row. If the HBM mappings drift
back to the broken shape — e.g., a <cache usage="read-only"/>
directive being reintroduced — the T3 read will return the stale T1 bag.
Note: BaseDatabaseTest5 runs H2 in-memory with
hibernate.cache.use_second_level_cache=false, so this test cannot
reproduce a live L2-cache staleness against an in-memory DB. It instead
asserts the architectural invariant that survives flush+clear cycles:
the AnalysisResultSet.results bag is bidirectional
(inverse="true" on the parent, FK driven by the child's
resultSet many-to-one), and a child written from a fresh session
IS visible to a subsequent fresh-session read. The L2-cache directive is
the production-only amplifier of the same underlying bag-staleness shape;
if the mapping invariants here drift, the L2 staleness path becomes live
again and this guard will trip first.
See docs/audit/HIBERNATE6_CASCADE_AUDIT.md finding #6. A bare @see cannot
name a Markdown file -- javadoc resolves its argument as a type or URL.
- See Also:
-
Nested Class Summary
Nested classes/interfaces inherited from class BaseDatabaseTest5
BaseDatabaseTest5.BaseDatabaseTestContextConfiguration -
Field Summary
Fields inherited from class BaseDatabaseTest5
sessionFactory -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidCompanion read-only assertion for the inner level —DifferentialExpressionAnalysisResult.getContrasts().voidCross-session-reload pin for the third leg of the02c87a91edfix: thehitListSizeschild bag onExpressionAnalysisResultSet.voidReproduction of HB6 audit finding #6: ensure that aDifferentialExpressionAnalysisResultwritten to an existingExpressionAnalysisResultSetafter a flush+clear is visible when the result set is reloaded from a fresh session.Methods inherited from class BaseDatabaseTest5
flushAndClearSession
-
Constructor Details
-
AnalysisResultSetCacheStalenessTest
public AnalysisResultSetCacheStalenessTest()
-
-
Method Details
-
testResultSetReflectsLaterAddedResultAcrossSessions
@Test public void testResultSetReflectsLaterAddedResultAcrossSessions()Reproduction of HB6 audit finding #6: ensure that aDifferentialExpressionAnalysisResultwritten to an existingExpressionAnalysisResultSetafter a flush+clear is visible when the result set is reloaded from a fresh session. -
testContrastsSurviveFlushClearReload
@Test public void testContrastsSurviveFlushClearReload()Companion read-only assertion for the inner level —DifferentialExpressionAnalysisResult.getContrasts(). The audit doc calls out that thecontrastsbag is alsomutable="false"inside amutable="false"parent and was the second level of the two-level cache-staleness chain. UnlikeAnalysisResultSet.results, thecontrastsbag is BOTHmutable="false"AND unidirectional with no back-reference onContrastResult: there is noContrastResult.setResult(...)/resultSet-style accessor. As a result Hibernate refuses to flush any cross-sessionreloadedResult.getContrasts().add(...)("changed an immutable collection instance"), and the new row could not carry the FK back-ref anyway. Production never adds contrasts after the fact: thecreate()path inDifferentialExpressionAnalysisDaoImplinserts contrasts together with their parent DEAResult via raw JDBC (seeINSERT_CONTRAST_SQL), so the "cross-tx-write a contrast" scenario is not a real code path in Gemma.This test therefore asserts only that the
contrastsbag survives a flush+clear+reload with the original contents intact — pinning that the inner-level cache directive does NOT come back. If a future change adds a back-reference and makescontrastswritable across sessions, this test should be upgraded to the full cross-session-add pattern used by the result-set test above. -
testHitListSizesSurviveFlushClearReload
@Test public void testHitListSizesSurviveFlushClearReload()Cross-session-reload pin for the third leg of the02c87a91edfix: thehitListSizeschild bag onExpressionAnalysisResultSet.Pre-fix, the parent set carried
<cache usage="read-only"/>on amutable="false"bag inside amutable="false"parent — the same shape as theresultsbag. The fix dropped the bag-level cache directive; this test pins that the bag survives flush+clear+reload with its persisted contents intact, so any regression that re-adds the read-only cache directive (or otherwise breaks bag reload semantics) is caught here in addition to theresults-bag guard above.HitListSizerows aremutable="false"and the bag is unidirectional with no setter back-reference, so the cross-session-ADD pattern used bytestResultSetReflectsLaterAddedResultAcrossSessions()does not apply (Hibernate refuses immutable-collection mutation). We therefore exercise the same shape used bytestContrastsSurviveFlushClearReload(): persist N hit-list-size rows together with the parent, flush+clear, and assert a fresh-session load reads back all N rows from the database (not from a stale empty cache snapshot).
-