blob: 9bb9fee3d59efdce4316f34f52730de54592b2d9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.internal.cache;
import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.apache.geode.LogWriter;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.InterestResultPolicy;
import org.apache.geode.cache.Operation;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.internal.cache.tier.InterestType;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.SerializableCallable;
import org.apache.geode.test.dunit.SerializableCallableIF;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.junit.categories.ClientServerTest;
/**
* This tests the fix for bug #43407 under a variety of configurations and also tests that
* tombstones are treated in a similar manner. The ticket complains that a client that does a get(K)
* does not end up with the entry in its cache if K is invalid on the server.
*/
@Category({ClientServerTest.class})
public class ClientServerInvalidAndDestroyedEntryDUnitTest extends JUnit4CacheTestCase {
@Override
public final void postSetUp() throws Exception {
disconnectAllFromDS();
}
@Test
public void testClientGetsInvalidEntry() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsInvalidEntry(regionName, false, false);
}
@Test
public void testClientGetsInvalidEntryPR() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsInvalidEntry(regionName, true, false);
}
@Test
public void testClientGetsTombstone() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsTombstone(regionName, false, false);
}
@Test
public void testClientGetsTombstonePR() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsTombstone(regionName, true, false);
}
// same tests but with transactions...
@Test
public void testClientGetsInvalidEntryTX() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsInvalidEntry(regionName, false, true);
}
@Test
public void testClientGetsInvalidEntryPRTX() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsInvalidEntry(regionName, true, true);
}
@Test
public void testClientGetsTombstoneTX() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsTombstone(regionName, false, true);
}
@Test
public void testClientGetsTombstonePRTX() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestClientGetsTombstone(regionName, true, true);
}
// tests for bug #46780, tombstones left in client after RI
@Test
public void testRegisterInterestRemovesOldEntry() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestRegisterInterestRemovesOldEntry(regionName, false);
}
@Test
public void testRegisterInterestRemovesOldEntryPR() throws Exception {
final String regionName = getUniqueName() + "Region";
doTestRegisterInterestRemovesOldEntry(regionName, true);
}
/* this method creates a server cache and is used by all of the tests in this class */
private SerializableCallableIF getCreateServerCallable(final String regionName,
final boolean usePR) {
return new SerializableCallable("create server and entries") {
@Override
public Object call() {
Cache cache = getCache();
List<CacheServer> servers = cache.getCacheServers();
CacheServer server;
if (servers.size() > 0) {
server = servers.get(0);
} else {
server = cache.addCacheServer();
int port = AvailablePortHelper.getRandomAvailableTCPPort();
server.setPort(port);
server.setHostnameForClients("localhost");
try {
server.start();
} catch (IOException e) {
Assert.fail("Failed to start server ", e);
}
}
if (usePR) {
RegionFactory factory = cache.createRegionFactory(RegionShortcut.PARTITION);
PartitionAttributesFactory pf = new PartitionAttributesFactory();
pf.setTotalNumBuckets(2);
factory.setPartitionAttributes(pf.create());
factory.create(regionName);
} else {
cache.createRegionFactory(RegionShortcut.REPLICATE).create(regionName);
}
return server.getPort();
}
};
}
/**
* Bug #43407 - when a client does a get(k) and the entry is invalid in the server we want the
* client to end up with an entry that is invalid.
*/
private void doTestClientGetsInvalidEntry(final String regionName, final boolean usePR,
boolean useTX) throws Exception {
VM vm1 = Host.getHost(0).getVM(1);
VM vm2 = Host.getHost(0).getVM(2);
// here are the keys that will be used to validate behavior. Keys must be
// colocated if using both a partitioned region in the server and transactions
// in the client. All of these keys hash to bucket 0 in a two-bucket PR
// except Object11 and IDoNotExist1
final String notAffectedKey = "Object1";
final String nonexistentKey = (usePR && useTX) ? "IDoNotExist2" : "IDoNotExist1";
final String key1 = "Object10";
final String key2 = (usePR && useTX) ? "Object12" : "Object11";
SerializableCallableIF createServer = getCreateServerCallable(regionName, usePR);
int serverPort = (Integer) vm1.invoke(createServer);
vm2.invoke(createServer);
vm1.invoke(new SerializableRunnable("populate server and create invalid entry") {
@Override
public void run() {
Region myRegion = getCache().getRegion(regionName);
for (int i = 1; i <= 20; i++) {
myRegion.put("Object" + i, "Value" + i);
}
myRegion.invalidate(key1);
myRegion.invalidate(key2);
}
});
org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("creating client cache");
ClientCache c = new ClientCacheFactory().addPoolServer("localhost", serverPort)
.set(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel()).create();
Region myRegion =
c.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY).create(regionName);
if (useTX) {
c.getCacheTransactionManager().begin();
}
// get of a valid entry should work
assertNotNull(myRegion.get(notAffectedKey));
// get of an invalid entry should return null and create the entry in an invalid state
org.apache.geode.test.dunit.LogWriterUtils.getLogWriter()
.info("getting " + key1 + " - should reach this cache and be INVALID");
assertNull(myRegion.get(key1));
assertTrue(myRegion.containsKey(key1));
// since this might be a PR we also check the next key to force PR Get messaging
assertNull(myRegion.get(key2));
assertTrue(myRegion.containsKey(key2));
// now try a key that doesn't exist anywhere
assertNull(myRegion.get(nonexistentKey));
assertFalse(myRegion.containsKey(nonexistentKey));
if (useTX) {
c.getCacheTransactionManager().commit();
// test that the commit correctly created the entries in the region
assertNotNull(myRegion.get(notAffectedKey));
assertNull(myRegion.get(key1));
assertTrue(myRegion.containsKey(key1));
assertNull(myRegion.get(key2));
assertTrue(myRegion.containsKey(key2));
}
myRegion.localDestroy(notAffectedKey);
myRegion.localDestroy(key1);
myRegion.localDestroy(key2);
if (useTX) {
c.getCacheTransactionManager().begin();
}
// check that getAll returns invalidated entries
List keys = new LinkedList();
keys.add(notAffectedKey);
keys.add(key1);
keys.add(key2);
Map result = myRegion.getAll(keys);
assertNotNull(result.get(notAffectedKey));
assertNull(result.get(key1));
assertNull(result.get(key2));
assertTrue(result.containsKey(key1));
assertTrue(result.containsKey(key2));
assertTrue(myRegion.containsKey(key1));
assertTrue(myRegion.containsKey(key2));
if (useTX) {
c.getCacheTransactionManager().commit();
// test that the commit correctly created the entries in the region
assertNotNull(myRegion.get(notAffectedKey));
assertNull(myRegion.get(key1));
assertTrue(myRegion.containsKey(key1));
assertNull(myRegion.get(key2));
assertTrue(myRegion.containsKey(key2));
}
// test that a listener is not invoked when there is already an invalidated
// entry in the client cache
UpdateListener listener = new UpdateListener();
listener.log = org.apache.geode.test.dunit.LogWriterUtils.getLogWriter();
myRegion.getAttributesMutator().addCacheListener(listener);
myRegion.get(key1);
assertEquals("expected no cache listener invocations", 0, listener.updateCount,
listener.updateCount);
myRegion.localDestroy(notAffectedKey);
myRegion.getAll(keys);
assertTrue("expected to find " + notAffectedKey, myRegion.containsKey(notAffectedKey));
assertEquals("expected only one listener invocation for " + notAffectedKey, 1,
listener.updateCount);
}
/**
* Similar to bug #43407 but not reported in a ticket, we want a client that does a get() on a
* destroyed entry to end up with a tombstone for that entry. This was already the case but there
* were no unit tests covering this for different server configurations and with/without
* transactions.
*/
private void doTestClientGetsTombstone(final String regionName, final boolean usePR,
boolean useTX) throws Exception {
VM vm1 = Host.getHost(0).getVM(1);
VM vm2 = Host.getHost(0).getVM(2);
// here are the keys that will be used to validate behavior. Keys must be
// colocated if using both a partitioned region in the server and transactions
// in the client. All of these keys hash to bucket 0 in a two-bucket PR
// except Object11 and IDoNotExist1
final String notAffectedKey = "Object1";
final String nonexistentKey = (usePR && useTX) ? "IDoNotExist2" : "IDoNotExist1";
final String key1 = "Object10";
final String key2 = (usePR && useTX) ? "Object12" : "Object11";
SerializableCallableIF createServer = getCreateServerCallable(regionName, usePR);
int serverPort = (Integer) vm1.invoke(createServer);
vm2.invoke(createServer);
vm1.invoke(new SerializableRunnable("populate server and create invalid entry") {
@Override
public void run() {
Region myRegion = getCache().getRegion(regionName);
for (int i = 1; i <= 20; i++) {
myRegion.put("Object" + i, "Value" + i);
}
myRegion.destroy(key1);
myRegion.destroy(key2);
}
});
org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("creating client cache");
ClientCache c = new ClientCacheFactory().addPoolServer("localhost", serverPort)
.set(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel()).create();
Region myRegion =
c.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY).create(regionName);
if (useTX) {
c.getCacheTransactionManager().begin();
}
// get of a valid entry should work
assertNotNull(myRegion.get(notAffectedKey));
// get of an invalid entry should return null and create the entry in an invalid state
org.apache.geode.test.dunit.LogWriterUtils.getLogWriter()
.info("getting " + key1 + " - should reach this cache and be a TOMBSTONE");
assertNull(myRegion.get(key1));
assertFalse(myRegion.containsKey(key1));
RegionEntry entry;
if (!useTX) {
entry = ((LocalRegion) myRegion).getRegionEntry(key1);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
}
// since this might be a PR we also check the next key to force PR Get messaging
assertNull(myRegion.get(key2));
assertFalse(myRegion.containsKey(key2));
if (!useTX) {
entry = ((LocalRegion) myRegion).getRegionEntry(key2);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
}
// now try a key that doesn't exist anywhere
assertNull(myRegion.get(nonexistentKey));
assertFalse(myRegion.containsKey(nonexistentKey));
if (useTX) {
c.getCacheTransactionManager().commit();
// test that the commit correctly created the entries in the region
assertNotNull(myRegion.get(notAffectedKey));
assertNull(myRegion.get(key1));
assertFalse(myRegion.containsKey(key1));
entry = ((LocalRegion) myRegion).getRegionEntry(key1);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
assertNull(myRegion.get(key2));
assertFalse(myRegion.containsKey(key2));
entry = ((LocalRegion) myRegion).getRegionEntry(key2);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
}
myRegion.localDestroy(notAffectedKey);
if (useTX) {
c.getCacheTransactionManager().begin();
}
// check that getAll returns invalidated entries
List keys = new LinkedList();
keys.add(notAffectedKey);
keys.add(key1);
keys.add(key2);
Map result = myRegion.getAll(keys);
org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("result of getAll = " + result);
assertNotNull(result.get(notAffectedKey));
assertNull(result.get(key1));
assertNull(result.get(key2));
assertFalse(myRegion.containsKey(key1));
assertFalse(myRegion.containsKey(key2));
if (!useTX) {
entry = ((LocalRegion) myRegion).getRegionEntry(key1);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
entry = ((LocalRegion) myRegion).getRegionEntry(key2);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
} else { // useTX
c.getCacheTransactionManager().commit();
// test that the commit correctly created the entries in the region
assertNotNull(myRegion.get(notAffectedKey));
assertNull(myRegion.get(key1));
assertFalse(myRegion.containsKey(key1));
entry = ((LocalRegion) myRegion).getRegionEntry(key1);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
assertNull(myRegion.get(key2));
assertFalse(myRegion.containsKey(key2));
entry = ((LocalRegion) myRegion).getRegionEntry(key2);
assertNotNull(entry); // it should be there
assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
}
}
private void doTestRegisterInterestRemovesOldEntry(final String regionName, final boolean usePR)
throws Exception {
VM vm1 = Host.getHost(0).getVM(1);
VM vm2 = Host.getHost(0).getVM(2);
// here are the keys that will be used to validate behavior. Keys must be
// colocated if using both a partitioned region in the server and transactions
// in the client. All of these keys hash to bucket 0 in a two-bucket PR
// except Object11 and IDoNotExist1
final String key10 = "Object10";
final String interestPattern = "Object.*";
SerializableCallableIF createServer = getCreateServerCallable(regionName, usePR);
int serverPort = (Integer) vm1.invoke(createServer);
vm2.invoke(createServer);
vm1.invoke(new SerializableRunnable("populate server") {
@Override
public void run() {
Region myRegion = getCache().getRegion(regionName);
for (int i = 1; i <= 20; i++) {
myRegion.put("Object" + i, "Value" + i);
}
}
});
org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("creating client cache");
ClientCache c = new ClientCacheFactory().addPoolServer("localhost", serverPort)
.set(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel()).setPoolSubscriptionEnabled(true)
.create();
Region myRegion =
c.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY).create(regionName);
myRegion.registerInterestRegex(interestPattern);
// make sure key1 is in the client because we're going to mess with it
assertNotNull(myRegion.get(key10));
// remove the entry for key1 on the servers and then simulate interest recovery
// to show that the entry for key1 is no longer there in the client when recovery
// finishes
SerializableRunnable destroyKey10 =
new SerializableRunnable("locally destroy " + key10 + " in the servers") {
@Override
public void run() {
Region myRegion = getCache().getRegion(regionName);
EntryEventImpl event = ((LocalRegion) myRegion).generateEvictDestroyEvent(key10);
event.setOperation(Operation.LOCAL_DESTROY);
if (usePR) {
BucketRegion bucket = ((PartitionedRegion) myRegion).getBucketRegion(key10);
if (bucket != null) {
event.setRegion(bucket);
org.apache.geode.test.dunit.LogWriterUtils.getLogWriter()
.info("performing local destroy in " + bucket + " ccEnabled="
+ bucket.getConcurrencyChecksEnabled() + " rvv="
+ bucket.getVersionVector());
bucket.setConcurrencyChecksEnabled(false); // turn off cc so entry is removed
bucket.mapDestroy(event, false, false, null);
bucket.setConcurrencyChecksEnabled(true);
}
} else {
((LocalRegion) myRegion).setConcurrencyChecksEnabled(false); // turn off cc so entry
// is
// removed
((LocalRegion) myRegion).mapDestroy(event, false, false, null);
((LocalRegion) myRegion).setConcurrencyChecksEnabled(true);
}
}
};
vm1.invoke(destroyKey10);
vm2.invoke(destroyKey10);
myRegion.getCache().getLogger().info("clearing keys of interest");
((LocalRegion) myRegion).clearKeysOfInterest(interestPattern, InterestType.REGULAR_EXPRESSION,
InterestResultPolicy.KEYS_VALUES);
myRegion.getCache().getLogger().info("done clearing keys of interest");
assertTrue("expected region to be empty but it has " + myRegion.size() + " entries",
myRegion.size() == 0);
RegionEntry entry;
entry = ((LocalRegion) myRegion).getRegionEntry(key10);
assertNull(entry); // it should have been removed
// now register interest. At the end, finishRegisterInterest should clear
// out the entry for key1 because it was stored in image-state as a
// destroyed RI entry in clearKeysOfInterest
myRegion.registerInterestRegex(interestPattern);
entry = ((LocalRegion) myRegion).getRegionEntry(key10);
assertNull(entry); // it should not be there
}
static class UpdateListener extends CacheListenerAdapter {
int updateCount;
LogWriter log;
@Override
public void afterUpdate(EntryEvent event) {
// log.info("UpdateListener.afterUpdate invoked for " + event, new Exception("stack trace"));
updateCount++;
}
@Override
public void afterCreate(EntryEvent event) {
// log.info("UpdateListener.afterCreate invoked for " + event, new Exception("stack trace"));
updateCount++;
}
}
}