/*
* 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.recordstore;
import com.hazelcast.config.MapConfig;
import com.hazelcast.core.EntryView;
import com.hazelcast.map.impl.MapContainer;
import com.hazelcast.map.impl.event.MapEventPublisher;
import com.hazelcast.map.impl.eviction.Evictor;
import com.hazelcast.map.impl.record.Record;
import com.hazelcast.nio.Address;
import com.hazelcast.nio.serialization.Data;
import com.hazelcast.spi.EventService;
import com.hazelcast.spi.NodeEngine;
import com.hazelcast.spi.properties.GroupProperty;
import com.hazelcast.spi.properties.HazelcastProperties;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import static com.hazelcast.core.EntryEventType.EVICTED;
import static com.hazelcast.core.EntryEventType.EXPIRED;
import static com.hazelcast.map.impl.ExpirationTimeSetter.calculateExpirationWithDelay;
import static com.hazelcast.map.impl.ExpirationTimeSetter.calculateMaxIdleMillis;
import static com.hazelcast.map.impl.ExpirationTimeSetter.getIdlenessStartTime;
import static com.hazelcast.map.impl.ExpirationTimeSetter.getLifeStartTime;
import static com.hazelcast.map.impl.ExpirationTimeSetter.setExpirationTime;
import static com.hazelcast.map.impl.MapService.SERVICE_NAME;
import static com.hazelcast.map.impl.eviction.Evictor.NULL_EVICTOR;
/**
* Contains eviction specific functionality.
*/
abstract class AbstractEvictableRecordStore extends AbstractRecordStore {
protected final long expiryDelayMillis;
protected final EventService eventService;
protected final MapEventPublisher mapEventPublisher;
protected final Address thisAddress;
/**
* Iterates over a pre-set entry count/percentage in one round.
* Used in expiration logic for traversing entries. Initializes lazily.
*/
protected Iterator<Record> expirationIterator;
protected volatile boolean hasEntryWithCustomTTL;
protected AbstractEvictableRecordStore(MapContainer mapContainer, int partitionId) {
super(mapContainer, partitionId);
NodeEngine nodeEngine = mapServiceContext.getNodeEngine();
HazelcastProperties hazelcastProperties = nodeEngine.getProperties();
expiryDelayMillis = hazelcastProperties.getMillis(GroupProperty.MAP_EXPIRY_DELAY_SECONDS);
eventService = nodeEngine.getEventService();
mapEventPublisher = mapServiceContext.getMapEventPublisher();
thisAddress = nodeEngine.getThisAddress();
}
/**
* Returns {@code true} if this record store has at least one candidate entry
* for expiration (idle or tll) otherwise returns {@code false}.
*/
private boolean isRecordStoreExpirable() {
final MapConfig mapConfig = mapContainer.getMapConfig();
return hasEntryWithCustomTTL || mapConfig.getMaxIdleSeconds() > 0
|| mapConfig.getTimeToLiveSeconds() > 0;
}
@Override
public void evictExpiredEntries(int percentage, boolean backup) {
final long now = getNow();
final int size = size();
final int maxIterationCount = getMaxIterationCount(size, percentage);
final int maxRetry = 3;
int loop = 0;
int evictedEntryCount = 0;
while (true) {
evictedEntryCount += evictExpiredEntriesInternal(maxIterationCount, now, backup);
if (evictedEntryCount >= maxIterationCount) {
break;
}
loop++;
if (loop > maxRetry) {
break;
}
}
}
@Override
public boolean isExpirable() {
return isRecordStoreExpirable();
}
/**
* Intended to put an upper bound to iterations. Used in evictions.
*
* @param size of iterate-able.
* @param percentage percentage of size.
* @return 100 If calculated iteration count is less than 100, otherwise returns calculated iteration count.
*/
private int getMaxIterationCount(int size, int percentage) {
final int defaultMaxIterationCount = 100;
final float oneHundred = 100F;
float maxIterationCount = size * (percentage / oneHundred);
if (maxIterationCount <= defaultMaxIterationCount) {
return defaultMaxIterationCount;
}
return Math.round(maxIterationCount);
}
private int evictExpiredEntriesInternal(int maxIterationCount, long now, boolean backup) {
int evictedCount = 0;
int checkedEntryCount = 0;
initExpirationIterator();
while (expirationIterator.hasNext()) {
if (checkedEntryCount >= maxIterationCount) {
break;
}
checkedEntryCount++;
Record record = expirationIterator.next();
record = getOrNullIfExpired(record, now, backup);
if (record == null) {
evictedCount++;
}
}
return evictedCount;
}
private void initExpirationIterator() {
if (expirationIterator == null || !expirationIterator.hasNext()) {
expirationIterator = storage.values().iterator();
}
}
@Override
public void evictEntries(Data excludedKey) {
if (shouldEvict()) {
mapContainer.getEvictor().evict(this, excludedKey);
}
}
@Override
public boolean shouldEvict() {
Evictor evictor = mapContainer.getEvictor();
return evictor != NULL_EVICTOR && evictor.checkEvictable(this);
}
protected void markRecordStoreExpirable(long ttl) {
if (ttl > 0L && ttl < Long.MAX_VALUE) {
hasEntryWithCustomTTL = true;
}
}
/**
* Check if record is reachable according to ttl or idle times.
* If not reachable return null.
*
* @param record {@link com.hazelcast.map.impl.record.Record}
* @return null if evictable.
*/
protected Record getOrNullIfExpired(Record record, long now, boolean backup) {
if (!isRecordStoreExpirable()) {
return record;
}
if (record == null) {
return null;
}
final Data key = record.getKey();
if (isLocked(key)) {
return record;
}
if (!isExpired(record, now, backup)) {
return record;
}
evict(key, backup);
if (!backup) {
doPostEvictionOperations(record, backup);
}
return null;
}
public boolean isExpired(Record record, long now, boolean backup) {
return record == null
|| isIdleExpired(record, now, backup) == null
|| isTTLExpired(record, now, backup) == null;
}
private Record isIdleExpired(Record record, long now, boolean backup) {
if (record == null) {
return null;
}
long maxIdleMillis = calculateMaxIdleMillis(mapContainer.getMapConfig());
if (maxIdleMillis == Long.MAX_VALUE) {
return record;
}
long idlenessStartTime = getIdlenessStartTime(record);
long idleMillis = calculateExpirationWithDelay(maxIdleMillis, expiryDelayMillis, backup);
long elapsedMillis = now - idlenessStartTime;
return elapsedMillis >= idleMillis ? null : record;
}
private Record isTTLExpired(Record record, long now, boolean backup) {
if (record == null) {
return null;
}
long ttl = record.getTtl();
// when ttl is zero or negative or Long.MAX_VALUE, it should remain eternally.
if (ttl < 1L || ttl == Long.MAX_VALUE) {
return record;
}
long ttlStartTime = getLifeStartTime(record);
long ttlMillis = calculateExpirationWithDelay(ttl, expiryDelayMillis, backup);
long elapsedMillis = now - ttlStartTime;
return elapsedMillis >= ttlMillis ? null : record;
}
@Override
public void doPostEvictionOperations(Record record, boolean backup) {
if (!eventService.hasEventRegistration(SERVICE_NAME, name)) {
return;
}
// Fire EVICTED event also in case of expiration because historically eviction-listener
// listens all kind of eviction and expiration events and by firing EVICTED event we are preserving
// this behavior.
Data key = record.getKey();
Object value = record.getValue();
mapEventPublisher.publishEvent(thisAddress, name, EVICTED, key, value, null);
if (isExpired(record, getNow(), backup)) {
// We will be in this if in two cases:
// 1. In case of TTL or max-idle-seconds expiration.
// 2. When evicting due to the size-based eviction, we are also firing an EXPIRED event
// because there is a possibility that evicted entry may be also an expired one. Trying to catch
// as much as possible expired entries.
mapEventPublisher.publishEvent(thisAddress, name, EXPIRED, key, value, null);
}
}
protected void accessRecord(Record record, long now) {
record.onAccess(now);
updateStatsOnGet(now);
long maxIdleMillis = calculateMaxIdleMillis(mapContainer.getMapConfig());
setExpirationTime(record, maxIdleMillis);
}
protected void mergeRecordExpiration(Record record, EntryView mergingEntry) {
final long ttlMillis = mergingEntry.getTtl();
record.setTtl(ttlMillis);
final long creationTime = mergingEntry.getCreationTime();
record.setCreationTime(creationTime);
final long lastAccessTime = mergingEntry.getLastAccessTime();
record.setLastAccessTime(lastAccessTime);
final long lastUpdateTime = mergingEntry.getLastUpdateTime();
record.setLastUpdateTime(lastUpdateTime);
final long maxIdleMillis = calculateMaxIdleMillis(mapContainer.getMapConfig());
setExpirationTime(record, maxIdleMillis);
markRecordStoreExpirable(record.getTtl());
}
/**
* Read only iterator. Iterates by checking whether a record expired or not.
*/
protected final class ReadOnlyRecordIterator implements Iterator<Record> {
private final long now;
private final boolean checkExpiration;
private final boolean backup;
private final Iterator<Record> iterator;
private Record nextRecord;
private Record lastReturned;
protected ReadOnlyRecordIterator(Collection<Record> values, long now, boolean backup) {
this(values, now, true, backup);
}
protected ReadOnlyRecordIterator(Collection<Record> values) {
this(values, -1L, false, false);
}
private ReadOnlyRecordIterator(Collection<Record> values, long now, boolean checkExpiration, boolean backup) {
this.iterator = values.iterator();
this.now = now;
this.checkExpiration = checkExpiration;
this.backup = backup;
advance();
}
@Override
public boolean hasNext() {
return nextRecord != null;
}
@Override
public Record next() {
if (nextRecord == null) {
throw new NoSuchElementException();
}
lastReturned = nextRecord;
advance();
return lastReturned;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove() is not supported by this iterator");
}
private void advance() {
final long now = this.now;
final boolean checkExpiration = this.checkExpiration;
final Iterator<Record> iterator = this.iterator;
while (iterator.hasNext()) {
nextRecord = iterator.next();
if (nextRecord != null) {
if (!checkExpiration) {
return;
}
if (!isExpired(nextRecord, now, backup)) {
return;
}
}
}
nextRecord = null;
}
}
}