blob: d4bd0cc89b890e68d4bcca6f6a9b58e325f1dd11 [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.versions;
import static org.apache.geode.cache.RegionShortcut.PARTITION_PERSISTENT;
import static org.apache.geode.cache.RegionShortcut.REPLICATE;
import static org.apache.geode.cache.RegionShortcut.REPLICATE_PERSISTENT;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPort;
import static org.apache.geode.internal.cache.InitialImageOperation.GIITestHookType.DuringApplyDelta;
import static org.apache.geode.internal.cache.InitialImageOperation.resetAllGIITestHooks;
import static org.apache.geode.internal.cache.TombstoneService.EXPIRED_TOMBSTONE_LIMIT;
import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import org.apache.logging.log4j.Logger;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.Region;
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.distributed.internal.ClusterDistributionManager;
import org.apache.geode.distributed.internal.DistributionMessage;
import org.apache.geode.distributed.internal.DistributionMessageObserver;
import org.apache.geode.internal.cache.DestroyOperation;
import org.apache.geode.internal.cache.DistributedTombstoneOperation;
import org.apache.geode.internal.cache.InitialImageOperation;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.RegionEntry;
import org.apache.geode.internal.cache.TombstoneService;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.NetworkUtils;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.rules.DistributedRule;
public class TombstoneDUnitTest implements Serializable {
private static final long serialVersionUID = 2992716917694662945L;
private static Cache cache;
private static Region<String, String> region;
final String REGION_NAME = "TestRegion";
@Rule
public DistributedRule distributedRule = new DistributedRule();
@After
public void close() {
for (VM vm : Arrays.asList(VM.getVM(0), VM.getVM(1))) {
vm.invoke(() -> {
resetAllGIITestHooks();
if (region != null && !region.isDestroyed()) {
region.destroyRegion();
}
region = null;
if (cache != null) {
cache.close();
}
});
}
}
@Test
public void testTombstoneGcMessagesBetweenPersistentAndNonPersistentRegion() {
VM vm0 = VM.getVM(0);
VM vm1 = VM.getVM(1);
vm0.invoke(() -> {
createCacheAndRegion(REPLICATE_PERSISTENT);
region.put("K1", "V1");
region.put("K2", "V2");
});
vm1.invoke(() -> createCacheAndRegion(REPLICATE));
vm0.invoke(() -> {
// Send tombstone gc message to vm1.
region.destroy("K1");
assertEquals(1, ((InternalCache) cache).getCachePerfStats().getTombstoneCount());
performGC(1);
});
vm1.invoke(() -> {
// After processing tombstone message from vm0. The tombstone count should be 0.
waitForTombstoneCount(0);
assertEquals(0, ((InternalCache) cache).getCachePerfStats().getTombstoneCount());
// Send tombstone gc message to vm0.
region.destroy("K2");
performGC(1);
});
vm0.invoke(() -> {
// After processing tombstone message from vm0. The tombstone count should be 0.
waitForTombstoneCount(0);
assertEquals(0, ((InternalCache) cache).getCachePerfStats().getTombstoneCount());
});
}
@Test
public void testTombstoneExpiredAndNonExpiredAreClearedAfterRegionIsDestroyed() {
VM vm0 = VM.getVM(0);
vm0.invoke(() -> {
// reduce timeout so that tombstone is immediately marked as expired
TombstoneService.REPLICATE_TOMBSTONE_TIMEOUT = 100;
createCacheAndRegion(PARTITION_PERSISTENT);
region.put("K1", "V1");
region.destroy("K1");
});
vm0.invoke(() -> {
waitForScheduledTombstoneCount(0);
// increase timeout so that next tombstone doesn't expire
TombstoneService.REPLICATE_TOMBSTONE_TIMEOUT = 150000;
region.put("K1", "V1");
region.destroy("K1");
region.destroyRegion();
// force expiry of batch - there is only one expired tombstone at this moment
EXPIRED_TOMBSTONE_LIMIT = 1;
});
vm0.invoke(() -> {
createCacheAndRegion(PARTITION_PERSISTENT);
checkExpiredTombstones(0);
});
}
@Test
public void testWhenAnOutOfRangeTimeStampIsSeenWeExpireItInReplicateTombstoneSweeper() {
VM server1 = VM.getVM(0);
VM server2 = VM.getVM(1);
final int FAR_INTO_THE_FUTURE = 1000000; // 1 million millis into the future
final int count = 10;
// Create a cache and load some boiler plate entries
server1.invoke(() -> {
createCacheAndRegion(RegionShortcut.REPLICATE);
for (int i = 0; i < count; i++) {
region.put("K" + i, "V" + i);
}
});
server2.invoke(() -> createCacheAndRegion(RegionShortcut.REPLICATE));
server1.invoke(() -> {
// Now that we have a cache and a region specifically with data, we can start the real work
TombstoneService.TombstoneSweeper tombstoneSweeper =
((InternalCache) cache).getTombstoneService().getSweeper((LocalRegion) region);
// Get one of the entries
RegionEntry regionEntry = ((LocalRegion) region).getRegionEntry("K0");
/*
* Create a version tag with a timestamp far off in the future...
* It should be in the near past, but we are testing that a future tombstone will be cleared
*/
VersionTag<?> versionTag = regionEntry.getVersionStamp().asVersionTag();
versionTag.setVersionTimeStamp(System.currentTimeMillis() + FAR_INTO_THE_FUTURE);
// Create the forged tombstone with the versionTag from the future
TombstoneService.Tombstone modifiedTombstone =
new TombstoneService.Tombstone(regionEntry, (LocalRegion) region,
versionTag);
// Add it to the list of tombstones so that when checkOldestUnexpired is called it will see it
tombstoneSweeper.tombstones.add(modifiedTombstone);
tombstoneSweeper.checkOldestUnexpired(System.currentTimeMillis());
// Validate that the tombstone was cleared.
assertThat(tombstoneSweeper.getOldestTombstoneTime()).isEqualTo(0);
});
}
@Test
public void testWhenAnOutOfRangeTimeStampIsSeenWeExpireItInNonReplicateTombstoneSweeper() {
VM server1 = VM.getVM(0);
VM server2 = VM.getVM(1);
final int FAR_INTO_THE_FUTURE = 1000000; // 1 million millis into the future
final int count = 2000;
Logger logger = LogService.getLogger();
// Create a cache and load some boiler plate entries
server1.invoke(() -> {
createCacheAndRegion(RegionShortcut.PARTITION);
for (int i = 0; i < count; i++) {
region.put("K" + i, "V" + i);
}
});
server2.invoke(() -> createCacheAndRegion(RegionShortcut.PARTITION));
server1.invoke(() -> {
// Now that we have a cache and a region specifically with data, we can start the real work
TombstoneService.TombstoneSweeper tombstoneSweeper =
((InternalCache) cache).getTombstoneService().getSweeper((LocalRegion) region);
// Get one of the entries
PartitionedRegion partitionedRegion = (PartitionedRegion) region;
RegionEntry regionEntry = partitionedRegion.getBucketRegion("K0").getRegionEntry("K0");
/*
* Create a version tag with a timestamp far off in the future...
* It should be in the near past, but we are testing that a future tombstone will be cleared
*/
VersionTag<?> versionTag = regionEntry.getVersionStamp().asVersionTag();
versionTag.setVersionTimeStamp(System.currentTimeMillis() + FAR_INTO_THE_FUTURE);
// Create the forged tombstone with the versionTag from the future
TombstoneService.Tombstone modifiedTombstone =
new TombstoneService.Tombstone(regionEntry, (LocalRegion) region,
versionTag);
// Add it to the list of tombstones so that when checkOldestUnexpired is called it will see it
tombstoneSweeper.tombstones.add(modifiedTombstone);
tombstoneSweeper.checkOldestUnexpired(System.currentTimeMillis());
// Validate that the tombstone was cleared.
assertThat(tombstoneSweeper.getOldestTombstoneTime()).isEqualTo(0);
});
}
@Test
public void testGetOldestTombstoneTimeForReplicateTombstoneSweeper() {
VM server1 = VM.getVM(0);
VM server2 = VM.getVM(1);
final int count = 10;
server1.invoke(() -> {
createCacheAndRegion(RegionShortcut.REPLICATE);
for (int i = 0; i < count; i++) {
region.put("K" + i, "V" + i);
}
});
server2.invoke(() -> createCacheAndRegion(RegionShortcut.REPLICATE));
server1.invoke(() -> {
TombstoneService.TombstoneSweeper tombstoneSweeper =
((InternalCache) cache).getTombstoneService().getSweeper((LocalRegion) region);
// Send tombstone gc message to vm1.
for (int i = 0; i < count; i++) {
region.destroy("K" + i);
}
await().untilAsserted(() -> assertThat(
tombstoneSweeper.getOldestTombstoneTime()
+ TombstoneService.REPLICATE_TOMBSTONE_TIMEOUT_DEFAULT - System.currentTimeMillis())
.isGreaterThan(0));
performGC(count);
assertThat(tombstoneSweeper.getOldestTombstoneTime()).isEqualTo(0);
});
}
@Test
public void testGetOldestTombstoneTimeForNonReplicateTombstoneSweeper() {
VM client = VM.getVM(0);
VM server = VM.getVM(1);
// Fire up the server and put in some data that is deletable
server.invoke(() -> {
createCacheAndRegion(REPLICATE);
int serverPort = getRandomAvailableTCPPort();
CacheServer cacheServer = cache.addCacheServer();
cacheServer.setPort(serverPort);
cacheServer.start();
for (int i = 0; i < 1000; i++) {
region.put("K" + i, "V" + i);
}
});
String locatorHost = NetworkUtils.getServerHostName();
int locatorPort = DistributedRule.getLocatorPort();
// Use the client to remove and entry, thus creating a tombstone
client.invoke(() -> {
createClientCacheAndRegion(locatorHost, locatorPort);
region.remove("K3");
});
// Validate that a tombstone was created and that it has a timestamp that is valid,
// Then GC and validate there is no oldest tombstone.
server.invoke(() -> {
TombstoneService.TombstoneSweeper tombstoneSweeper =
((InternalCache) cache).getTombstoneService().getSweeper((LocalRegion) region);
assertThat(tombstoneSweeper.getOldestTombstoneTime()).isGreaterThan(0)
.isLessThan(((InternalCache) cache).cacheTimeMillis());
performGC(1);
assertThat(tombstoneSweeper.getOldestTombstoneTime()).isEqualTo(0);
});
}
/**
* The purpose of this test is for a replicate region scenario to get the oldest tombstone
* and validate that it matches the tombstone of the entry we removed.
*/
@Test
public void testGetOldestTombstoneForReplicateTombstoneSweeper() {
VM server1 = VM.getVM(0);
VM server2 = VM.getVM(1);
server1.invoke(() -> {
createCacheAndRegion(REPLICATE);
region.put("K1", "V1");
region.put("K2", "V2");
});
server2.invoke(() -> createCacheAndRegion(REPLICATE));
server1.invoke(() -> {
// Send tombstone gc message to vm1.
region.destroy("K1");
TombstoneService.TombstoneSweeper tombstoneSweeper =
((InternalCache) cache).getTombstoneService().getSweeper((LocalRegion) region);
assertThat(tombstoneSweeper.getOldestTombstone()).contains("K1");
performGC(1);
assertThat(tombstoneSweeper.getOldestTombstone()).isNull();
});
}
/**
* The purpose of this test is for a non-replicate region scenario to get the oldest tombstone
* and validate that it matches the tombstone of the entry we removed. This is done through a
* client
* as a client is required to have this non-replicate tombstone.
*/
@Test
public void testGetOldestTombstoneForNonReplicateTombstoneSweeper() {
VM client = VM.getVM(0);
VM server = VM.getVM(1);
// Fire up the server and put in some data that is deletable
server.invoke(() -> {
createCacheAndRegion(REPLICATE);
int serverPort = getRandomAvailableTCPPort();
CacheServer cacheServer = cache.addCacheServer();
cacheServer.setPort(serverPort);
cacheServer.start();
for (int i = 0; i < 1000; i++) {
region.put("K" + i, "V" + i);
}
});
String locatorHost = NetworkUtils.getServerHostName();
int locatorPort = DistributedRule.getLocatorPort();
// Use the client to remove and entry, thus creating a tombstone
client.invoke(() -> {
createClientCacheAndRegion(locatorHost, locatorPort);
region.remove("K3");
});
// Validate that a tombstone was created and that it has a timestamp that is valid,
// Then GC and validate there is no oldest tombstone.
server.invoke(() -> {
TombstoneService.TombstoneSweeper tombstoneSweeper =
((InternalCache) cache).getTombstoneService().getSweeper((LocalRegion) region);
assertThat(tombstoneSweeper.getOldestTombstone()).contains("K3");
performGC(1);
assertThat(tombstoneSweeper.getOldestTombstone()).isNull();
});
}
@Test
public void testTombstonesWithLowerVersionThanTheRecordedVersionGetsGCedAfterDestroy()
throws Exception {
VM vm0 = VM.getVM(0);
VM vm1 = VM.getVM(1);
Properties props = DistributedRule.getDistributedSystemProperties();
props.setProperty("conserve-sockets", "false");
vm0.invoke(() -> {
createCacheAndRegion(REPLICATE_PERSISTENT, props);
region.put("K1", "V1");
region.put("K2", "V2");
});
vm1.invoke(() -> {
createCacheAndRegion(REPLICATE, props);
DistributionMessageObserver.setInstance(new RegionObserver());
});
AsyncInvocation<Object> vm0Async1 = vm0.invokeAsync(() -> {
region.destroy("K1");
});
AsyncInvocation<Object> vm0Async2 = vm0.invokeAsync(() -> {
region.destroy("K2");
});
AsyncInvocation<Object> vm0Async3 = vm0.invokeAsync(() -> {
waitForTombstoneCount(2);
performGC(2);
});
vm1.invoke(() -> await()
.until(() -> ((InternalCache) cache).getCachePerfStats().getTombstoneGCCount() == 1));
vm0Async1.await();
vm0Async2.await();
vm0Async3.await();
vm1.invoke(() -> {
performGC(((LocalRegion) region).getTombstoneCount());
assertEquals(0, ((LocalRegion) region).getTombstoneCount());
});
}
@Test
public void tombstoneGCDuringGIICorrectlySchedulesTombstonesForCollection() {
VM vm0 = VM.getVM(0);
VM vm1 = VM.getVM(1);
Properties props = DistributedRule.getDistributedSystemProperties();
props.setProperty("conserve-sockets", "false");
vm0.invoke(() -> {
createCacheAndRegion(REPLICATE_PERSISTENT, props);
region.put("K1", "V1");
region.put("K2", "V2");
});
vm1.invoke(() -> {
createCacheAndRegion(REPLICATE_PERSISTENT, props);
// Ensure that there are local tombstones to be recovered in the member that will request GII
region.destroy("K1");
region.destroy("K2");
cache.close();
region = null;
});
vm0.invoke(() -> {
// Ensure that there are newer tombstones that will be sent via GII
region.put("K1", "V3");
region.destroy("K1");
region.put("K2", "V4");
region.destroy("K2");
// Trigger a tombstone GC after receiving the GII request message
InitialImageOperation.setGIITestHook(new InitialImageOperation.GIITestHook(
InitialImageOperation.GIITestHookType.AfterReceivedRequestImage, REGION_NAME) {
private static final long serialVersionUID = -3790198435185240444L;
@Override
public void reset() {}
@Override
public void run() {
try {
performGC(((LocalRegion) region).getTombstoneCount());
} catch (Exception ignore) {
}
}
});
});
vm1.invoke(() -> {
cache = new CacheFactory(props).create();
InitialImageOperation.setGIITestHook(
new InitialImageOperation.GIITestHook(DuringApplyDelta, REGION_NAME) {
private static final long serialVersionUID = 637083883125364247L;
private int entryNumber = 0;
@Override
public void reset() {}
@Override
public void run() {
if (entryNumber == 0) {
await().alias("Waiting for scheduled tombstone count to be zero")
.until(() -> ((InternalCache) cache).getTombstoneService()
.getScheduledTombstoneCount() == 0);
}
// Confirm that tombstones are correctly scheduled for collection after processing
// each new entry received during GII
assertThat(((InternalCache) cache).getTombstoneService().getScheduledTombstoneCount())
.as("Scheduled tombstone count did not match expected value")
.isEqualTo(entryNumber++);
}
});
region = cache.<String, String>createRegionFactory(REPLICATE_PERSISTENT).create(REGION_NAME);
// Confirm that we are able to collect all tombstones once the region is initialized
performGC(((LocalRegion) region).getTombstoneCount());
assertEquals(0, ((LocalRegion) region).getTombstoneCount());
});
}
// Client Cache
private void createClientCacheAndRegion(String locatorHost, int locatorPort) {
ClientCache clientcache =
new ClientCacheFactory().addPoolLocator(locatorHost, locatorPort).create();
region = clientcache.<String, String>createClientRegionFactory(
ClientRegionShortcut.PROXY).create(REGION_NAME);
}
// Server Cache
private void createCacheAndRegion(RegionShortcut replicatePersistent, Properties properties) {
cache = (new CacheFactory(properties)).create();
region = cache.<String, String>createRegionFactory(replicatePersistent).create(REGION_NAME);
}
private void createCacheAndRegion(RegionShortcut regionShortCut) {
createCacheAndRegion(regionShortCut, DistributedRule.getDistributedSystemProperties());
}
private static class RegionObserver extends DistributionMessageObserver implements Serializable {
private static final long serialVersionUID = 6272522949825923089L;
VersionTag<?> versionTag;
CountDownLatch tombstoneGcLatch = new CountDownLatch(1);
@Override
public void beforeProcessMessage(ClusterDistributionManager dm, DistributionMessage message) {
// Allow destroy with higher version to complete first.
if (message instanceof DestroyOperation.DestroyMessage) {
// wait for tombstoneGC message to complete.
try {
tombstoneGcLatch.await();
synchronized (this) {
DestroyOperation.DestroyMessage destroyMessage =
(DestroyOperation.DestroyMessage) message;
if (versionTag == null) {
// First destroy
versionTag = destroyMessage.getVersionTag();
wait();
} else {
// Second destroy
if (destroyMessage.getVersionTag().getRegionVersion() < versionTag
.getRegionVersion()) {
notifyAll();
wait();
}
}
}
} catch (InterruptedException ignored) {
}
}
}
@Override
public void afterProcessMessage(ClusterDistributionManager dm, DistributionMessage message) {
if (message instanceof DestroyOperation.DestroyMessage) {
// Notify the destroy with smaller version to continue.
synchronized (this) {
notifyAll();
}
}
if (message instanceof DistributedTombstoneOperation.TombstoneMessage) {
tombstoneGcLatch.countDown();
}
}
}
private void waitForTombstoneCount(int count) {
try {
await().until(() -> ((InternalCache) cache).getCachePerfStats().getTombstoneCount() == count);
} catch (Exception e) {
// The caller to throw exception with proper message.
}
}
private void waitForScheduledTombstoneCount(int count) {
LocalRegion region = (LocalRegion) cache.getRegion(REGION_NAME);
await().until(() -> ((InternalCache) cache).getTombstoneService().getSweeper(region).tombstones
.size() == count);
}
private void checkExpiredTombstones(int count) {
await().until(
() -> ((InternalCache) cache).getTombstoneService().getScheduledTombstoneCount() == count);
}
private void performGC(int count) throws Exception {
((InternalCache) cache).getTombstoneService().forceBatchExpirationForTests(count);
}
}