Skip to content

Commit 1d62197

Browse files
rvansasebersole
authored andcommitted
HHH-10008 SessionImplementor.getTimestamp() does not return transaction start time;
HHH-9962 Second level query cache returns stale data if query and update statements are executed concurrently
1 parent 1376b12 commit 1d62197

File tree

4 files changed

+141
-4
lines changed

4 files changed

+141
-4
lines changed

hibernate-core/src/main/java/org/hibernate/cache/internal/StandardQueryCache.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,13 @@ public boolean put(
111111
if ( isNaturalKeyLookup && result.isEmpty() ) {
112112
return false;
113113
}
114-
final long ts = cacheRegion.nextTimestamp();
115-
116114
if ( DEBUGGING ) {
117-
LOG.debugf( "Caching query results in region: %s; timestamp=%s", cacheRegion.getName(), ts );
115+
LOG.debugf( "Caching query results in region: %s; timestamp=%s", cacheRegion.getName(), session.getTimestamp() );
118116
}
119117

120118
final List cacheable = new ArrayList( result.size() + 1 );
121119
logCachedResultDetails( key, null, returnTypes, cacheable );
122-
cacheable.add( ts );
120+
cacheable.add( session.getTimestamp() );
123121

124122
final boolean isSingleResult = returnTypes.length == 1;
125123
for ( Object aResult : result ) {

hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,10 @@ public void setCacheMode(CacheMode cacheMode) {
14361436
public Transaction beginTransaction() throws HibernateException {
14371437
errorIfClosed();
14381438
Transaction result = getTransaction();
1439+
// begin on already started transaction is noop, therefore, don't update the timestamp
1440+
if (result.getStatus() != TransactionStatus.ACTIVE) {
1441+
timestamp = factory.getSettings().getRegionFactory().nextTimestamp();
1442+
}
14391443
result.begin();
14401444
return result;
14411445
}

hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,10 @@ public void setFlushMode(FlushMode fm) {
528528
public Transaction beginTransaction() throws HibernateException {
529529
errorIfClosed();
530530
Transaction result = getTransaction();
531+
// begin on already started transaction is noop, therefore, don't update the timestamp
532+
if (result.getStatus() != TransactionStatus.ACTIVE) {
533+
timestamp = factory.getSettings().getRegionFactory().nextTimestamp();
534+
}
531535
result.begin();
532536
return result;
533537
}

hibernate-core/src/test/java/org/hibernate/test/querycache/QueryCacheTest.java

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,24 @@
66
*/
77
package org.hibernate.test.querycache;
88

9+
import java.io.Serializable;
910
import java.util.ArrayList;
1011
import java.util.List;
1112
import java.util.Map;
13+
import java.util.concurrent.Callable;
14+
import java.util.concurrent.CountDownLatch;
15+
import java.util.concurrent.ExecutionException;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.Future;
1219

1320
import org.hibernate.Criteria;
21+
import org.hibernate.EmptyInterceptor;
1422
import org.hibernate.Hibernate;
1523
import org.hibernate.Query;
1624
import org.hibernate.SQLQuery;
1725
import org.hibernate.Session;
26+
import org.hibernate.SessionBuilder;
1827
import org.hibernate.Transaction;
1928
import org.hibernate.cfg.AvailableSettings;
2029
import org.hibernate.criterion.Restrictions;
@@ -26,6 +35,7 @@
2635
import org.hibernate.testing.RequiresDialectFeature;
2736
import org.hibernate.testing.TestForIssue;
2837
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
38+
import org.hibernate.type.Type;
2939
import org.junit.Test;
3040

3141
import static org.junit.Assert.assertEquals;
@@ -39,6 +49,7 @@
3949
public class QueryCacheTest extends BaseNonConfigCoreFunctionalTestCase {
4050

4151
private static final CompositeKey PK = new CompositeKey(1, 2);
52+
private static final ExecutorService executor = Executors.newFixedThreadPool(4);
4253

4354
@Override
4455
public String[] getMappings() {
@@ -63,6 +74,17 @@ protected void addSettings(Map settings) {
6374
settings.put( AvailableSettings.GENERATE_STATISTICS, "true" );
6475
}
6576

77+
@Override
78+
protected void shutDown() {
79+
super.shutDown();
80+
executor.shutdown();
81+
}
82+
83+
@Override
84+
protected boolean isCleanupTestDataRequired() {
85+
return true;
86+
}
87+
6688
@Override
6789
protected String getCacheConcurrencyStrategy() {
6890
return "nonstrict-read-write";
@@ -529,5 +551,114 @@ public void testScalarSQLQuery() {
529551
// assertEquals(1, query.getResultList().size());
530552
// }
531553

554+
@Test
555+
@TestForIssue(jiraKey = "HHH-9962")
556+
/* Test courtesy of Giambattista Bloisi */
557+
public void testDelayedLoad() throws InterruptedException, ExecutionException {
558+
DelayLoadOperations interceptor = new DelayLoadOperations();
559+
final SessionBuilder sessionBuilder = sessionFactory().withOptions().interceptor(interceptor);
560+
Item item1 = new Item();
561+
item1.setName("Item1");
562+
item1.setDescription("Washington");
563+
Session s1 = sessionBuilder.openSession();
564+
Transaction tx1 = s1.beginTransaction();
565+
s1.persist(item1);
566+
tx1.commit();
567+
s1.close();
568+
569+
Item item2 = new Item();
570+
item2.setName("Item2");
571+
item2.setDescription("Chicago");
572+
Session s2 = sessionBuilder.openSession();
573+
Transaction tx2 = s2.beginTransaction();
574+
s2.persist(item2);
575+
tx2.commit();
576+
s2.close();
577+
578+
interceptor.blockOnLoad();
579+
580+
Future<Item> fetchedItem = executor.submit(new Callable<Item>() {
581+
public Item call() throws Exception {
582+
return findByDescription(sessionBuilder, "Washington");
583+
}
584+
});
585+
586+
// wait for the onLoad listener to be called
587+
interceptor.waitOnLoad();
588+
589+
Session s3 = sessionBuilder.openSession();
590+
Transaction tx3 = s3.beginTransaction();
591+
item1.setDescription("New York");
592+
item2.setDescription("Washington");
593+
s3.update(item1);
594+
s3.update(item2);
595+
tx3.commit();
596+
s3.close();
597+
598+
interceptor.unblockOnLoad();
599+
600+
// the concurrent query was executed before the data was amended so
601+
// let's expect "Item1" to be returned as living in Washington
602+
Item fetched = fetchedItem.get();
603+
assertEquals("Item1", fetched.getName());
604+
605+
// Query again: now "Item2" is expected to live in Washington
606+
fetched = findByDescription(sessionBuilder, "Washington");
607+
assertEquals("Item2", fetched.getName());
608+
}
609+
610+
protected Item findByDescription(SessionBuilder sessionBuilder, final String description) {
611+
Session s = sessionBuilder.openSession();
612+
try {
613+
return (Item) s.createCriteria(Item.class)
614+
.setCacheable(true)
615+
.setReadOnly(true)
616+
.add(Restrictions.eq("description", description))
617+
.uniqueResult();
618+
619+
} finally {
620+
s.close();
621+
}
622+
}
623+
624+
public class DelayLoadOperations extends EmptyInterceptor {
625+
626+
private volatile CountDownLatch blockLatch;
627+
private volatile CountDownLatch waitLatch;
628+
629+
@Override
630+
public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
631+
// Synchronize load and update activities
632+
try {
633+
if (waitLatch != null) {
634+
waitLatch.countDown();
635+
waitLatch = null;
636+
}
637+
if (blockLatch != null) {
638+
blockLatch.await();
639+
blockLatch = null;
640+
}
641+
} catch (InterruptedException e) {
642+
Thread.currentThread().interrupt();
643+
throw new RuntimeException(e);
644+
}
645+
return true;
646+
}
647+
648+
public void blockOnLoad() {
649+
blockLatch = new CountDownLatch(1);
650+
waitLatch = new CountDownLatch(1);
651+
}
652+
653+
public void waitOnLoad() throws InterruptedException {
654+
waitLatch.await();
655+
}
656+
657+
public void unblockOnLoad() {
658+
if (blockLatch != null) {
659+
blockLatch.countDown();
660+
}
661+
}
662+
}
532663
}
533664

0 commit comments

Comments
 (0)