/*
* 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.internal.nearcache.impl.store;
import com.hazelcast.config.EvictionConfig;
import com.hazelcast.config.EvictionPolicy;
import com.hazelcast.config.NearCacheConfig;
import com.hazelcast.core.IFunction;
import com.hazelcast.internal.eviction.EvictionChecker;
import com.hazelcast.internal.eviction.EvictionListener;
import com.hazelcast.internal.eviction.impl.evaluator.EvictionPolicyEvaluator;
import com.hazelcast.internal.eviction.impl.strategy.sampling.SamplingEvictionStrategy;
import com.hazelcast.internal.nearcache.NearCacheRecord;
import com.hazelcast.internal.nearcache.NearCacheRecordStore;
import com.hazelcast.internal.nearcache.impl.SampleableNearCacheRecordMap;
import com.hazelcast.internal.nearcache.impl.invalidation.MetaDataContainer;
import com.hazelcast.internal.nearcache.impl.invalidation.StaleReadDetector;
import com.hazelcast.monitor.NearCacheStats;
import com.hazelcast.monitor.impl.NearCacheStatsImpl;
import com.hazelcast.nio.serialization.Data;
import com.hazelcast.nio.serialization.SerializableByConvention;
import com.hazelcast.spi.serialization.SerializationService;
import com.hazelcast.util.Clock;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import static com.hazelcast.internal.eviction.EvictionPolicyEvaluatorProvider.getEvictionPolicyEvaluator;
import static com.hazelcast.internal.memory.GlobalMemoryAccessorRegistry.MEM;
import static com.hazelcast.internal.memory.GlobalMemoryAccessorRegistry.MEM_AVAILABLE;
import static com.hazelcast.internal.nearcache.NearCacheRecord.NOT_RESERVED;
import static com.hazelcast.internal.nearcache.NearCacheRecord.READ_PERMITTED;
import static com.hazelcast.internal.nearcache.NearCacheRecord.RESERVED;
import static com.hazelcast.internal.nearcache.NearCacheRecord.UPDATE_STARTED;
import static com.hazelcast.internal.nearcache.impl.invalidation.StaleReadDetector.ALWAYS_FRESH;
import static com.hazelcast.util.ExceptionUtil.rethrow;
import static java.util.concurrent.atomic.AtomicLongFieldUpdater.newUpdater;
/**
* Abstract implementation of {@link NearCacheRecordStore} and {@link EvictionListener}.
*
* @param <K> the type of the key stored in Near Cache
* @param <V> the type of the value stored in Near Cache
* @param <KS> the type of the key of the underlying {@link com.hazelcast.internal.nearcache.impl.NearCacheRecordMap}
* @param <R> the type of the value of the underlying {@link com.hazelcast.internal.nearcache.impl.NearCacheRecordMap}
* @param <NCRM> the type of the underlying {@link com.hazelcast.internal.nearcache.impl.NearCacheRecordMap}
*/
@SuppressWarnings("checkstyle:methodcount")
public abstract class AbstractNearCacheRecordStore<K, V, KS, R extends NearCacheRecord,
NCRM extends SampleableNearCacheRecordMap<KS, R>>
implements NearCacheRecordStore<K, V>, EvictionListener<KS, R> {
protected static final AtomicLongFieldUpdater<AbstractNearCacheRecordStore> RESERVATION_ID
= newUpdater(AbstractNearCacheRecordStore.class, "reservationId");
/**
* If Unsafe is available, Object array index scale (every index represents a reference)
* can be assumed as reference size.
* <p>
* Otherwise, we assume reference size as integer size that means
* we assume 32 bit JVM or compressed-references enabled 64 bit JVM
* by ignoring compressed-references disable mode on 64 bit JVM.
*/
protected static final int REFERENCE_SIZE = MEM_AVAILABLE ? MEM.arrayIndexScale(Object[].class) : (Integer.SIZE / Byte.SIZE);
protected static final int MILLI_SECONDS_IN_A_SECOND = 1000;
protected final long timeToLiveMillis;
protected final long maxIdleMillis;
protected final NearCacheConfig nearCacheConfig;
protected final SerializationService serializationService;
protected final ClassLoader classLoader;
protected final NearCacheStatsImpl nearCacheStats;
protected final IFunction<K, R> reserveForUpdate = new ReserveForUpdateFunction();
protected final boolean evictionDisabled;
protected EvictionChecker evictionChecker;
protected SamplingEvictionStrategy<KS, R, NCRM> evictionStrategy;
protected EvictionPolicyEvaluator<KS, R> evictionPolicyEvaluator;
protected NCRM records;
protected volatile StaleReadDetector staleReadDetector = ALWAYS_FRESH;
protected volatile long reservationId;
public AbstractNearCacheRecordStore(NearCacheConfig nearCacheConfig, SerializationService serializationService,
ClassLoader classLoader) {
this(nearCacheConfig, new NearCacheStatsImpl(), serializationService, classLoader);
}
// extended in EE
protected AbstractNearCacheRecordStore(NearCacheConfig nearCacheConfig, NearCacheStatsImpl nearCacheStats,
SerializationService serializationService, ClassLoader classLoader) {
this.nearCacheConfig = nearCacheConfig;
this.timeToLiveMillis = nearCacheConfig.getTimeToLiveSeconds() * MILLI_SECONDS_IN_A_SECOND;
this.maxIdleMillis = nearCacheConfig.getMaxIdleSeconds() * MILLI_SECONDS_IN_A_SECOND;
this.serializationService = serializationService;
this.classLoader = classLoader;
this.nearCacheStats = nearCacheStats;
this.evictionDisabled = nearCacheConfig.getEvictionConfig().getEvictionPolicy() == EvictionPolicy.NONE;
}
@Override
public void initialize() {
this.records = createNearCacheRecordMap(nearCacheConfig);
EvictionConfig evictionConfig = nearCacheConfig.getEvictionConfig();
this.evictionChecker = createNearCacheEvictionChecker(evictionConfig, nearCacheConfig);
if (!evictionDisabled) {
this.evictionStrategy = SamplingEvictionStrategy.INSTANCE;
this.evictionPolicyEvaluator = getEvictionPolicyEvaluator(evictionConfig, classLoader);
}
}
@Override
public void setStaleReadDetector(StaleReadDetector staleReadDetector) {
this.staleReadDetector = staleReadDetector;
}
@Override
public StaleReadDetector getStaleReadDetector() {
return staleReadDetector;
}
@Override
public abstract R getRecord(K key);
protected abstract EvictionChecker createNearCacheEvictionChecker(EvictionConfig evictionConfig,
NearCacheConfig nearCacheConfig);
protected abstract NCRM createNearCacheRecordMap(NearCacheConfig nearCacheConfig);
protected abstract long getKeyStorageMemoryCost(K key);
protected abstract long getRecordStorageMemoryCost(R record);
protected abstract R valueToRecord(V value);
protected abstract void updateRecordValue(R record, V value);
protected abstract V recordToValue(R record);
protected abstract R getOrCreateToReserve(K key);
protected abstract V updateAndGetReserved(K key, V value, long reservationId, boolean deserialize);
protected abstract R putRecord(K key, R record);
protected abstract R removeRecord(K key);
protected abstract boolean containsRecordKey(K key);
protected void checkAvailable() {
if (!isAvailable()) {
throw new IllegalStateException(nearCacheConfig.getName() + " named Near Cache record store is not available");
}
}
protected boolean isAvailable() {
return records != null;
}
protected Data valueToData(V value) {
if (value instanceof Data) {
return (Data) value;
} else if (value != null) {
return serializationService.toData(value);
} else {
return null;
}
}
protected V dataToValue(Data data) {
if (data != null) {
return serializationService.toObject(data);
} else {
return null;
}
}
protected Data toData(Object obj) {
if (obj == null) {
return null;
} else if (obj instanceof Data) {
return (Data) obj;
} else {
return valueToData((V) obj);
}
}
protected V toValue(Object obj) {
if (obj == null) {
return null;
} else if (obj instanceof Data) {
return dataToValue((Data) obj);
} else {
return (V) obj;
}
}
protected long getTotalStorageMemoryCost(K key, R record) {
return getKeyStorageMemoryCost(key) + getRecordStorageMemoryCost(record);
}
protected boolean isRecordExpired(R record) {
long now = Clock.currentTimeMillis();
if (record.isExpiredAt(now)) {
return true;
} else {
return record.isIdleAt(maxIdleMillis, now);
}
}
@SuppressWarnings("unused")
protected void onGet(K key, V value, R record) {
}
@SuppressWarnings("unused")
protected void onGetError(K key, V value, R record, Throwable error) {
}
@SuppressWarnings("unused")
protected void onPut(K key, V value, R record, R oldRecord) {
}
@SuppressWarnings("unused")
protected void onPutError(K key, V value, R record, R oldRecord, Throwable error) {
}
@SuppressWarnings("unused")
protected void onRemove(K key, R record, boolean removed) {
}
@SuppressWarnings("unused")
protected void onRemoveError(K key, R record, boolean removed, Throwable error) {
}
@SuppressWarnings("unused")
protected void onExpire(K key, R record) {
nearCacheStats.incrementExpirations();
}
@Override
public void onEvict(KS key, R record, boolean wasExpired) {
if (wasExpired) {
nearCacheStats.incrementExpirations();
} else {
nearCacheStats.incrementEvictions();
}
nearCacheStats.decrementOwnedEntryCount();
}
@Override
public V get(K key) {
checkAvailable();
R record = null;
V value = null;
try {
record = getRecord(key);
if (record != null) {
if (record.getRecordState() != READ_PERMITTED) {
return null;
}
if (staleReadDetector.isStaleRead(key, record)) {
remove(key);
return null;
}
if (isRecordExpired(record)) {
remove(key);
onExpire(key, record);
return null;
}
onRecordAccess(record);
nearCacheStats.incrementHits();
value = recordToValue(record);
onGet(key, value, record);
return value;
} else {
nearCacheStats.incrementMisses();
return null;
}
} catch (Throwable error) {
onGetError(key, value, record, error);
throw rethrow(error);
}
}
@Override
public void put(K key, V value) {
checkAvailable();
// if there is no eviction configured we return if the Near Cache is full and it's a new key
// (we have to check the key, otherwise we might lose updates on existing keys)
if (evictionDisabled && evictionChecker.isEvictionRequired() && !containsRecordKey(key)) {
return;
}
R record = null;
R oldRecord = null;
try {
record = valueToRecord(value);
onRecordCreate(key, record);
oldRecord = putRecord(key, record);
if (oldRecord == null) {
nearCacheStats.incrementOwnedEntryCount();
}
onPut(key, value, record, oldRecord);
} catch (Throwable error) {
onPutError(key, value, record, oldRecord, error);
throw rethrow(error);
}
}
@Override
public boolean remove(K key) {
checkAvailable();
R record = null;
boolean removed = false;
try {
record = removeRecord(key);
if (record != null && record.getRecordState() == READ_PERMITTED) {
removed = true;
nearCacheStats.decrementOwnedEntryCount();
}
onRemove(key, record, removed);
return record != null;
} catch (Throwable error) {
onRemoveError(key, record, removed, error);
throw rethrow(error);
}
}
@Override
public void clear() {
checkAvailable();
records.clear();
nearCacheStats.setOwnedEntryCount(0);
nearCacheStats.setOwnedEntryMemoryCost(0L);
}
@Override
public void destroy() {
clear();
}
@Override
public NearCacheStats getNearCacheStats() {
checkAvailable();
return nearCacheStats;
}
@Override
public int size() {
checkAvailable();
return records.size();
}
@Override
public void doEvictionIfRequired() {
checkAvailable();
if (!evictionDisabled) {
evictionStrategy.evict(records, evictionPolicyEvaluator, evictionChecker, this);
}
}
@Override
public void doEviction() {
checkAvailable();
if (!evictionDisabled) {
evictionStrategy.evict(records, evictionPolicyEvaluator, null, this);
}
}
@Override
public long tryReserveForUpdate(K key) {
checkAvailable();
// if there is no eviction configured we return if the Near Cache is full and it's a new key
// (we have to check the key, otherwise we might lose updates on existing keys)
if (evictionDisabled && evictionChecker.isEvictionRequired() && !containsRecordKey(key)) {
return NOT_RESERVED;
}
R reservedRecord = getOrCreateToReserve(key);
long reservationId = nextReservationId();
if (reservedRecord.casRecordState(RESERVED, reservationId)) {
return reservationId;
} else {
return NOT_RESERVED;
}
}
@Override
public V tryPublishReserved(K key, final V value, final long reservationId, boolean deserialize) {
return updateAndGetReserved(key, value, reservationId, deserialize);
}
protected R updateReservedRecordInternal(K key, V value, R reservedRecord, long reservationId) {
if (!reservedRecord.casRecordState(reservationId, UPDATE_STARTED)) {
return reservedRecord;
}
updateRecordValue(reservedRecord, value);
reservedRecord.casRecordState(UPDATE_STARTED, READ_PERMITTED);
nearCacheStats.incrementOwnedEntryMemoryCost(getTotalStorageMemoryCost(key, reservedRecord));
nearCacheStats.incrementOwnedEntryCount();
return reservedRecord;
}
private void onRecordAccess(R record) {
record.setAccessTime(Clock.currentTimeMillis());
record.incrementAccessHit();
}
private void onRecordCreate(K key, R record) {
record.setCreationTime(Clock.currentTimeMillis());
MetaDataContainer metaDataContainer = staleReadDetector.getMetaDataContainer(key);
if (metaDataContainer != null) {
record.setUuid(metaDataContainer.getUuid());
record.setInvalidationSequence(metaDataContainer.getSequence());
}
}
private long nextReservationId() {
return RESERVATION_ID.incrementAndGet(this);
}
@SerializableByConvention
private class ReserveForUpdateFunction implements IFunction<K, R> {
@Override
public R apply(K key) {
R record = null;
try {
record = valueToRecord(null);
onRecordCreate(key, record);
record.casRecordState(READ_PERMITTED, RESERVED);
} catch (Throwable throwable) {
onPutError(key, null, record, null, throwable);
throw rethrow(throwable);
}
return record;
}
}
}