Skip to content

Commit cc61914

Browse files
rvansagalderz
authored andcommitted
HHH-10185 In nonstrict-read-write mode the remove may be not applied
1 parent 9a9fb43 commit cc61914

File tree

5 files changed

+123
-15
lines changed

5 files changed

+123
-15
lines changed

hibernate-infinispan/src/main/java/org/hibernate/cache/infinispan/access/NonStrictAccessDelegate.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
*/
3333
public class NonStrictAccessDelegate implements AccessDelegate {
3434
private static final Log log = LogFactory.getLog( NonStrictAccessDelegate.class );
35+
private static final boolean trace = log.isTraceEnabled();
3536

3637
private final BaseTransactionalDataRegion region;
3738
private final AdvancedCache cache;
@@ -90,10 +91,17 @@ public boolean putFromLoad(SessionImplementor session, Object key, Object value,
9091
Object oldVersion = getVersion(prev);
9192
if (oldVersion != null) {
9293
if (versionComparator.compare(version, oldVersion) <= 0) {
94+
if (trace) {
95+
log.tracef("putFromLoad not executed since version(%s) <= oldVersion(%s)", version, oldVersion);
96+
}
9397
return false;
9498
}
9599
}
96100
else if (prev instanceof VersionedEntry && txTimestamp <= ((VersionedEntry) prev).getTimestamp()) {
101+
if (trace) {
102+
log.tracef("putFromLoad not executed since tx started at %d and entry was invalidated at %d",
103+
txTimestamp, ((VersionedEntry) prev).getTimestamp());
104+
}
97105
return false;
98106
}
99107
}
@@ -119,11 +127,13 @@ public boolean update(SessionImplementor session, Object key, Object value, Obje
119127

120128
@Override
121129
public void remove(SessionImplementor session, Object key) throws CacheException {
122-
Object value = cache.get(key);
123-
Object version = getVersion(value);
124130
// there's no 'afterRemove', so we have to use our own synchronization
131+
// the API does not provide version of removed item but we can't load it from the cache
132+
// as that would be prone to race conditions - if the entry was updated in the meantime
133+
// the remove could be discarded and we would end up with stale record
134+
// See VersionedTest#testCollectionUpdate for such situation
125135
TransactionCoordinator transactionCoordinator = session.getTransactionCoordinator();
126-
RemovalSynchronization sync = new RemovalSynchronization(transactionCoordinator, writeCache, false, region, key, version);
136+
RemovalSynchronization sync = new RemovalSynchronization(transactionCoordinator, writeCache, false, region, key);
127137
transactionCoordinator.getLocalSynchronizations().registerSynchronization(sync);
128138
}
129139

hibernate-infinispan/src/main/java/org/hibernate/cache/infinispan/access/RemovalSynchronization.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,17 @@
2020
public class RemovalSynchronization extends InvocationAfterCompletion {
2121
private final BaseTransactionalDataRegion region;
2222
private final Object key;
23-
private final Object version;
2423

25-
public RemovalSynchronization(TransactionCoordinator tc, AdvancedCache cache, boolean requiresTransaction, BaseTransactionalDataRegion region, Object key, Object version) {
24+
public RemovalSynchronization(TransactionCoordinator tc, AdvancedCache cache, boolean requiresTransaction, BaseTransactionalDataRegion region, Object key) {
2625
super(tc, cache, requiresTransaction);
2726
this.region = region;
2827
this.key = key;
29-
this.version = version;
3028
}
3129

3230
@Override
3331
protected void invoke(boolean success, AdvancedCache cache) {
3432
if (success) {
35-
if (version == null) {
36-
cache.put(key, new VersionedEntry(null, null, region.nextTimestamp()), region.getTombstoneExpiration(), TimeUnit.MILLISECONDS);
37-
}
38-
else {
39-
cache.put(key, new VersionedEntry(null, version, Long.MIN_VALUE), region.getTombstoneExpiration(), TimeUnit.MILLISECONDS);
40-
}
33+
cache.put(key, new VersionedEntry(null, null, region.nextTimestamp()), region.getTombstoneExpiration(), TimeUnit.MILLISECONDS);
4134
}
4235
}
4336
}

hibernate-infinispan/src/main/java/org/hibernate/cache/infinispan/access/VersionedCallInterceptor.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@
1818
import org.infinispan.factories.annotations.Inject;
1919
import org.infinispan.filter.NullValueConverter;
2020
import org.infinispan.interceptors.CallInterceptor;
21-
import org.infinispan.util.logging.Log;
22-
import org.infinispan.util.logging.LogFactory;
2321

2422
import java.util.Comparator;
2523
import java.util.Set;
26-
import java.util.UUID;
2724

2825
/**
2926
* Note that this does not implement all commands, only those appropriate for {@link TombstoneAccessDelegate}

hibernate-infinispan/src/test/java/org/hibernate/test/cache/infinispan/functional/AbstractNonInvalidationTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ protected void startUp() {
7373

7474
@Before
7575
public void insertAndClearCache() throws Exception {
76+
region = sessionFactory().getSecondLevelCacheRegion(Item.class.getName());
77+
entityCache = ((EntityRegionImpl) region).getCache();
7678
Item item = new Item("my item", "Original item");
7779
withTxSession(s -> s.persist(item));
7880
entityCache.clear();

hibernate-infinispan/src/test/java/org/hibernate/test/cache/infinispan/functional/VersionedTest.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33
import org.hibernate.PessimisticLockException;
44
import org.hibernate.Session;
55
import org.hibernate.StaleStateException;
6+
import org.hibernate.cache.infinispan.impl.BaseTransactionalDataRegion;
67
import org.hibernate.cache.infinispan.util.Caches;
78
import org.hibernate.cache.infinispan.util.VersionedEntry;
89
import org.hibernate.cache.spi.entry.CacheEntry;
910
import org.hibernate.engine.spi.SessionImplementor;
1011
import org.hibernate.test.cache.infinispan.functional.entities.Item;
12+
import org.hibernate.test.cache.infinispan.functional.entities.OtherItem;
13+
import org.infinispan.AdvancedCache;
14+
import org.infinispan.commands.write.PutKeyValueCommand;
1115
import org.infinispan.commons.util.ByRef;
16+
import org.infinispan.context.Flag;
17+
import org.infinispan.context.InvocationContext;
18+
import org.infinispan.interceptors.base.BaseCustomInterceptor;
1219
import org.junit.Test;
1320

1421
import javax.transaction.Synchronization;
@@ -17,10 +24,12 @@
1724
import java.util.Collections;
1825
import java.util.List;
1926
import java.util.Map;
27+
import java.util.Set;
2028
import java.util.concurrent.CountDownLatch;
2129
import java.util.concurrent.CyclicBarrier;
2230
import java.util.concurrent.Future;
2331
import java.util.concurrent.TimeUnit;
32+
import java.util.concurrent.atomic.AtomicBoolean;
2433
import java.util.concurrent.atomic.AtomicReference;
2534
import java.util.function.BiConsumer;
2635

@@ -41,6 +50,11 @@ public List<Object[]> getParameters() {
4150
return Arrays.asList(NONSTRICT_REPLICATED, NONSTRICT_DISTRIBUTED);
4251
}
4352

53+
@Override
54+
protected boolean getUseQueryCache() {
55+
return false;
56+
}
57+
4458
@Test
4559
public void testTwoRemoves() throws Exception {
4660
CyclicBarrier loadBarrier = new CyclicBarrier(2);
@@ -220,6 +234,98 @@ public void testEvictUpdateExpiration() throws Exception {
220234
assertSingleCacheEntry();
221235
}
222236

237+
@Test
238+
public void testCollectionUpdate() throws Exception {
239+
// the first insert puts VersionedEntry(null, null, timestamp), so we have to wait a while to cache the entry
240+
TIME_SERVICE.advance(1);
241+
242+
withTxSession(s -> {
243+
Item item = s.load(Item.class, itemId);
244+
OtherItem otherItem = new OtherItem();
245+
otherItem.setName("Other 1");
246+
s.persist(otherItem);
247+
item.addOtherItem(otherItem);
248+
});
249+
withTxSession(s -> {
250+
Item item = s.load(Item.class, itemId);
251+
Set<OtherItem> otherItems = item.getOtherItems();
252+
assertFalse(otherItems.isEmpty());
253+
otherItems.remove(otherItems.iterator().next());
254+
});
255+
256+
AdvancedCache collectionCache = ((BaseTransactionalDataRegion) sessionFactory().getSecondLevelCacheRegion(Item.class.getName() + ".otherItems")).getCache();
257+
CountDownLatch putFromLoadLatch = new CountDownLatch(1);
258+
AtomicBoolean committing = new AtomicBoolean(false);
259+
CollectionUpdateTestInterceptor collectionUpdateTestInterceptor = new CollectionUpdateTestInterceptor(putFromLoadLatch);
260+
AnotherCollectionUpdateTestInterceptor anotherInterceptor = new AnotherCollectionUpdateTestInterceptor(putFromLoadLatch, committing);
261+
collectionCache.addInterceptor(collectionUpdateTestInterceptor, collectionCache.getInterceptorChain().size() - 1);
262+
collectionCache.addInterceptor(anotherInterceptor, 0);
263+
264+
TIME_SERVICE.advance(1);
265+
Future<Boolean> addFuture = executor.submit(() -> withTxSessionApply(s -> {
266+
collectionUpdateTestInterceptor.updateLatch.await();
267+
Item item = s.load(Item.class, itemId);
268+
OtherItem otherItem = new OtherItem();
269+
otherItem.setName("Other 2");
270+
s.persist(otherItem);
271+
item.addOtherItem(otherItem);
272+
committing.set(true);
273+
return true;
274+
}));
275+
276+
Future<Boolean> readFuture = executor.submit(() -> withTxSessionApply(s -> {
277+
Item item = s.load(Item.class, itemId);
278+
assertTrue(item.getOtherItems().isEmpty());
279+
return true;
280+
}));
281+
282+
addFuture.get();
283+
readFuture.get();
284+
collectionCache.removeInterceptor(CollectionUpdateTestInterceptor.class);
285+
collectionCache.removeInterceptor(AnotherCollectionUpdateTestInterceptor.class);
286+
287+
withTxSession(s -> assertFalse(s.load(Item.class, itemId).getOtherItems().isEmpty()));
288+
}
289+
290+
private class CollectionUpdateTestInterceptor extends BaseCustomInterceptor {
291+
final AtomicBoolean firstPutFromLoad = new AtomicBoolean(true);
292+
final CountDownLatch putFromLoadLatch;
293+
final CountDownLatch updateLatch = new CountDownLatch(1);
294+
295+
public CollectionUpdateTestInterceptor(CountDownLatch putFromLoadLatch) {
296+
this.putFromLoadLatch = putFromLoadLatch;
297+
}
298+
299+
@Override
300+
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
301+
if (command.hasFlag(Flag.ZERO_LOCK_ACQUISITION_TIMEOUT)) {
302+
if (firstPutFromLoad.compareAndSet(true, false)) {
303+
updateLatch.countDown();
304+
putFromLoadLatch.await();
305+
}
306+
}
307+
return super.visitPutKeyValueCommand(ctx, command);
308+
}
309+
}
310+
311+
private class AnotherCollectionUpdateTestInterceptor extends BaseCustomInterceptor {
312+
final CountDownLatch putFromLoadLatch;
313+
final AtomicBoolean committing;
314+
315+
public AnotherCollectionUpdateTestInterceptor(CountDownLatch putFromLoadLatch, AtomicBoolean committing) {
316+
this.putFromLoadLatch = putFromLoadLatch;
317+
this.committing = committing;
318+
}
319+
320+
@Override
321+
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
322+
if (committing.get() && !command.hasFlag(Flag.ZERO_LOCK_ACQUISITION_TIMEOUT)) {
323+
putFromLoadLatch.countDown();
324+
}
325+
return super.visitPutKeyValueCommand(ctx, command);
326+
}
327+
}
328+
223329
protected void assertSingleEmpty() {
224330
Map contents = Caches.entrySet(entityCache).toMap();
225331
Object value;

0 commit comments

Comments
 (0)