/*
* Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved.
*
* Licensed 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 com.hazelcast.map.impl.eviction;
import com.hazelcast.config.Config;
import com.hazelcast.map.impl.PartitionContainer;
import com.hazelcast.map.impl.operation.ClearExpiredOperation;
import com.hazelcast.map.impl.recordstore.RecordStore;
import com.hazelcast.nio.Address;
import com.hazelcast.spi.ExecutionService;
import com.hazelcast.spi.NodeEngine;
import com.hazelcast.spi.Operation;
import com.hazelcast.spi.impl.operationservice.InternalOperationService;
import com.hazelcast.spi.partition.IPartition;
import com.hazelcast.spi.partition.IPartitionService;
import com.hazelcast.util.Clock;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import static com.hazelcast.map.impl.MapService.SERVICE_NAME;
import static com.hazelcast.util.CollectionUtil.isEmpty;
import static com.hazelcast.util.Preconditions.checkPositive;
import static com.hazelcast.util.Preconditions.checkTrue;
import static java.lang.Integer.parseInt;
import static java.lang.Math.min;
import static java.util.Collections.sort;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* This class is responsible for gradual cleanup of expired entries. For this purpose it uses a background task.
* <p>
* This background task can be accelerated or can be slowed down by using the system properties below:
* <p>
* <ul>
* <li>
* {@value com.hazelcast.map.impl.eviction.ExpirationManager#SYS_PROP_EXPIRATION_TASK_PERIOD_SECONDS}: A new round is
* started after this period of seconds.
* Default value is {@value com.hazelcast.map.impl.eviction.ExpirationManager#DEFAULT_EXPIRATION_TASK_PERIOD_SECONDS}
* seconds. So every {@value com.hazelcast.map.impl.eviction.ExpirationManager#DEFAULT_EXPIRATION_TASK_PERIOD_SECONDS}
* seconds there will be a new round.
* </li>
* <li>
* {@value com.hazelcast.map.impl.eviction.ExpirationManager#SYS_PROP_EXPIRATION_CLEANUP_PERCENTAGE}: Scannable percentage
* of entries in a maps' partition in each round.
* Default percentage is {@value com.hazelcast.map.impl.eviction.ExpirationManager#DEFAULT_EXPIRATION_CLEANUP_PERCENTAGE}%.
* </li>
* <li>
* {@value com.hazelcast.map.impl.eviction.ExpirationManager#SYS_PROP_EXPIRATION_CLEANUP_OPERATION_COUNT}: Number of
* scannable partitions in each round. No default value exists. Dynamically calculated against partition-count or
* partition-thread-count.
* </li>
*</ul>
* <p>
* These parameters can be set node-wide or system-wide
* <p>
* Node-wide setting example:
* <pre>
* Config config = new Config();
* config.setProperty(
* {@value com.hazelcast.map.impl.eviction.ExpirationManager#SYS_PROP_EXPIRATION_CLEANUP_OPERATION_COUNT}, "3");
* Hazelcast.newHazelcastInstance(config);
* </pre>
* </p>
* <p>
* System-wide setting example:
* <pre>
* System.setProperty(
* {@value com.hazelcast.map.impl.eviction.ExpirationManager#SYS_PROP_EXPIRATION_CLEANUP_OPERATION_COUNT}, "3");
* </pre>
* </p>
*
* @since 3.3
*/
public final class ExpirationManager {
public static final int DEFAULT_EXPIRATION_TASK_PERIOD_SECONDS = 5;
public static final int DEFAULT_EXPIRATION_CLEANUP_PERCENTAGE = 10;
public static final int DIFFERENCE_BETWEEN_TWO_SUBSEQUENT_PARTITION_CLEANUP_MILLIS = 1000;
@SuppressWarnings("checkstyle:linelength")
public static final String SYS_PROP_EXPIRATION_CLEANUP_OPERATION_COUNT = "hazelcast.internal.map.expiration.cleanup.operation.count";
public static final String SYS_PROP_EXPIRATION_TASK_PERIOD_SECONDS = "hazelcast.internal.map.expiration.task.period.seconds";
public static final String SYS_PROP_EXPIRATION_CLEANUP_PERCENTAGE = "hazelcast.internal.map.expiration.cleanup.percentage";
private final NodeEngine nodeEngine;
private final PartitionContainer[] partitionContainers;
private final Address thisAddress;
private final IPartitionService partitionService;
private final ExecutionService executionService;
private final InternalOperationService operationService;
private final int partitionCount;
private final int taskPeriodSeconds;
private final int cleanupPercentage;
private final int cleanupOperationCount;
private ScheduledFuture<?> expirationTask;
@SuppressWarnings("checkstyle:magicnumber")
@SuppressFBWarnings({"EI_EXPOSE_REP2"})
public ExpirationManager(PartitionContainer[] partitionContainers, NodeEngine nodeEngine) {
this.nodeEngine = nodeEngine;
this.partitionContainers = partitionContainers;
this.thisAddress = nodeEngine.getThisAddress();
this.partitionService = nodeEngine.getPartitionService();
this.executionService = nodeEngine.getExecutionService();
this.operationService = (InternalOperationService) nodeEngine.getOperationService();
this.partitionCount = partitionService.getPartitionCount();
this.taskPeriodSeconds = getInteger(SYS_PROP_EXPIRATION_TASK_PERIOD_SECONDS, DEFAULT_EXPIRATION_TASK_PERIOD_SECONDS);
checkPositive(taskPeriodSeconds, "taskPeriodSeconds should be a positive number");
this.cleanupPercentage = getInteger(SYS_PROP_EXPIRATION_CLEANUP_PERCENTAGE, DEFAULT_EXPIRATION_CLEANUP_PERCENTAGE);
checkTrue(cleanupPercentage > 0 && cleanupPercentage <= 100, "cleanupPercentage should be in range (0,100]");
int defaultCleanupOpCount = calculateCleanupOperationCount(partitionCount, operationService.getPartitionThreadCount());
this.cleanupOperationCount = getInteger(SYS_PROP_EXPIRATION_CLEANUP_OPERATION_COUNT, defaultCleanupOpCount);
checkPositive(cleanupOperationCount, "cleanupOperationCount should be a positive number");
}
public synchronized void start() {
if (expirationTask != null) {
return;
}
ClearExpiredRecordsTask task = new ClearExpiredRecordsTask();
expirationTask = executionService.getGlobalTaskScheduler().
scheduleWithRepetition(task, taskPeriodSeconds, taskPeriodSeconds, SECONDS);
}
public synchronized void stop() {
if (expirationTask == null) {
return;
}
expirationTask.cancel(true);
expirationTask = null;
}
private int getInteger(String propertyName, int defaultValue) {
Config config = nodeEngine.getConfig();
String property = config.getProperty(propertyName);
return property == null ? defaultValue : parseInt(property);
}
private static int calculateCleanupOperationCount(int partitionCount, int partitionThreadCount) {
// calculate operation count to be sent by using partition-count.
final double scanPercentage = 0.1D;
final int opCountFromPartitionCount = (int) (partitionCount * scanPercentage);
// calculate operation count to be sent by using partition-thread-count.
final int inflationFactor = 3;
int opCountFromThreadCount = partitionThreadCount * inflationFactor;
if (opCountFromPartitionCount == 0) {
return opCountFromThreadCount;
}
return min(opCountFromPartitionCount, opCountFromThreadCount);
}
/**
* Periodically clears expired entries.(ttl & idle)
* This task provides per partition expiration operation logic. (not per map, not per record store).
* Fires cleanup operations at most partition operation thread count or some factor of it in one round.
*/
private class ClearExpiredRecordsTask implements Runnable {
private final Comparator<PartitionContainer> partitionContainerComparator = new Comparator<PartitionContainer>() {
@Override
public int compare(PartitionContainer o1, PartitionContainer o2) {
final long s1 = o1.getLastCleanupTimeCopy();
final long s2 = o2.getLastCleanupTimeCopy();
return (s1 < s2) ? -1 : ((s1 == s2) ? 0 : 1);
}
};
@Override
public void run() {
final long now = Clock.currentTimeMillis();
int currentlyRunningCleanupOperationsCount = 0;
List<PartitionContainer> partitionContainers = null;
for (int partitionId = 0; partitionId < partitionCount; partitionId++) {
IPartition partition = partitionService.getPartition(partitionId, false);
if (partition.isOwnerOrBackup(thisAddress)) {
PartitionContainer partitionContainer = ExpirationManager.this.partitionContainers[partitionId];
if (isContainerEmpty(partitionContainer)) {
continue;
}
if (partitionContainer.hasRunningCleanup()) {
currentlyRunningCleanupOperationsCount++;
continue;
}
if (currentlyRunningCleanupOperationsCount > cleanupOperationCount
|| notInProcessableTimeWindow(partitionContainer, now)
|| notHaveAnyExpirableRecord(partitionContainer)) {
continue;
}
if (partitionContainers == null) {
partitionContainers = new ArrayList<PartitionContainer>();
}
partitionContainers.add(partitionContainer);
}
}
if (isEmpty(partitionContainers)) {
return;
}
sortPartitionContainers(partitionContainers);
sendCleanupOperations(partitionContainers);
}
private void sortPartitionContainers(List<PartitionContainer> partitionContainers) {
updateLastCleanupTimesBeforeSorting(partitionContainers);
sort(partitionContainers, partitionContainerComparator);
}
private void sendCleanupOperations(List<PartitionContainer> partitionContainers) {
final int start = 0;
int end = cleanupOperationCount;
if (end > partitionContainers.size()) {
end = partitionContainers.size();
}
List<PartitionContainer> partitionIds = partitionContainers.subList(start, end);
for (PartitionContainer container : partitionIds) {
// mark partition container as has on going expiration operation.
container.setHasRunningCleanup(true);
Operation operation = createExpirationOperation(cleanupPercentage, container.getPartitionId());
operationService.execute(operation);
}
}
private boolean notInProcessableTimeWindow(PartitionContainer partitionContainer, long now) {
return now - partitionContainer.getLastCleanupTime() < DIFFERENCE_BETWEEN_TWO_SUBSEQUENT_PARTITION_CLEANUP_MILLIS;
}
private boolean isContainerEmpty(PartitionContainer container) {
long size = 0L;
ConcurrentMap<String, RecordStore> maps = container.getMaps();
for (RecordStore store : maps.values()) {
size += store.size();
if (size > 0L) {
return false;
}
}
return true;
}
/**
* Here we check if that partition has any expirable record or not,
* if no expirable record exists in that partition no need to fire an expiration operation.
*
* @param partitionContainer corresponding partition container.
* @return <code>true</code> if no expirable record in that partition <code>false</code> otherwise.
*/
private boolean notHaveAnyExpirableRecord(PartitionContainer partitionContainer) {
boolean notExist = true;
final ConcurrentMap<String, RecordStore> maps = partitionContainer.getMaps();
for (RecordStore store : maps.values()) {
if (store.isExpirable()) {
notExist = false;
break;
}
}
return notExist;
}
}
private Operation createExpirationOperation(int expirationPercentage, int partitionId) {
return new ClearExpiredOperation(expirationPercentage)
.setNodeEngine(nodeEngine)
.setCallerUuid(nodeEngine.getLocalMember().getUuid())
.setPartitionId(partitionId)
.setValidateTarget(false)
.setServiceName(SERVICE_NAME);
}
/**
* Sets last clean-up time before sorting.
*
* @param partitionContainers partition containers.
*/
private void updateLastCleanupTimesBeforeSorting(List<PartitionContainer> partitionContainers) {
for (PartitionContainer partitionContainer : partitionContainers) {
partitionContainer.setLastCleanupTimeCopy(partitionContainer.getLastCleanupTime());
}
}
// used for testing purposes
int getTaskPeriodSeconds() {
return taskPeriodSeconds;
}
// used for testing purposes
int getCleanupPercentage() {
return cleanupPercentage;
}
// used for testing purposes
int getCleanupOperationCount() {
return cleanupOperationCount;
}
}