/*
* 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.cache.impl;
import com.hazelcast.nio.serialization.Data;
import javax.cache.Cache;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
/**
* {@link AbstractClusterWideIterator} provides the core iterator functionality shared by its descendants.
* <p>
* <p>Hazelcast cluster is made of partitions which holds a slice of all clusters data. Partition count
* never increase or decrease in a cluster. In order to implement an iterator over a partitioned data, we use
* the following parameters.
* <ul>
* <li>To iterate over partitioned data, we use partitionId as the first parameter of this iterator.</li>
* <li>Each partition may have a lot of entries, so we use a second parameter to track the iteration of the
* partition.</li>
* </ul>
* </p>
* <p>
* Iteration steps:
* <ul>
* <li>fetching fixed sized of keys from the current partition defined by partitionId.</li>
* <li>iteration on fetched keys.</li>
* <li>get value of each key with {@link #next()} when method is called.</li>
* <li>when fetched keys are all used by calling {@link #next()}, more keys are fetched from the cluster.</li>
* </ul>
* This implementation iterates over partitions and for each partition it iterates over the internal map using the
* internal table index of the map {@link com.hazelcast.util.SampleableConcurrentHashMap}.
* </p>
* <p>
* <h2>Fetching data from cluster:</h2>
* Fetching is getting a fixed size of keys from the internal table of records of a partition defined by
* partitionId. Table index is also provided as a table index locator. Fetch response is the keys and
* last table index. The last table index is included in the result to be used in the next fetch.
* </p>
* <p>
* <h2>Notes:</h2>
* <ul>
* <li>Iterator fetches keys in batch with a fixed size that is configurable.</li>
* <li>Fetched keys are cached in the iterator to be used in each iteration step.</li>
* <li>{@link #hasNext()} may return true for a key already removed.</li>
* <li>{@link #hasNext()} only return false when all known keys are fetched and iterated.</li>
* <li>{@link #next()} may return null although cache never has null value. This may happen when, for example,
* someone removes the entry after the current thread has checked with {@link #hasNext()}.</li>
* <li>This implementation does not affected by value updates as each value is got from the cluster
* when {@link #next()} called.</li>
* </ul>
* </p>
*
* @param <K> the type of key.
* @param <V> the type of value.
* @see com.hazelcast.cache.impl.CacheRecordStore#fetchKeys(int tableIndex, int size)
* @see com.hazelcast.cache.impl.ClusterWideIterator
* @see CacheKeyIterationResult
*/
public abstract class AbstractClusterWideIterator<K, V> implements Iterator<Cache.Entry<K, V>> {
protected static final int DEFAULT_FETCH_SIZE = 100;
protected ICacheInternal<K, V> cache;
protected List result;
protected final int partitionCount;
protected int partitionIndex = -1;
/**
* The table is segment table of hash map, which is an array that stores the actual records.
* This field is used to mark where the latest entry is read.
* <p>
* The iteration will start from highest index available to the table.
* It will be converted to array size on the server side.
*/
protected int lastTableIndex = Integer.MAX_VALUE;
protected final int fetchSize;
protected boolean prefetchValues;
protected int index;
protected int currentIndex = -1;
public AbstractClusterWideIterator(ICacheInternal<K, V> cache, int partitionCount, int fetchSize, boolean prefetchValues) {
this.cache = cache;
this.partitionCount = partitionCount;
this.fetchSize = fetchSize;
this.prefetchValues = prefetchValues;
}
@Override
public boolean hasNext() {
ensureOpen();
if (result != null && index < result.size()) {
return true;
}
return advance();
}
@Override
public Cache.Entry<K, V> next() {
while (hasNext()) {
currentIndex = index;
index++;
final Data keyData = getKey(currentIndex);
final K key = toObject(keyData);
final V value = getValue(currentIndex, key);
// Value might be removed or evicted
if (value != null) {
return new CacheEntry<K, V>(key, value);
}
}
throw new NoSuchElementException();
}
@Override
public void remove() {
ensureOpen();
if (result == null || currentIndex < 0) {
throw new IllegalStateException("Iterator.next() must be called before remove()!");
}
Data keyData = getKey(currentIndex);
final K key = toObject(keyData);
cache.remove(key);
currentIndex = -1;
}
protected boolean advance() {
while (partitionIndex < getPartitionCount()) {
if (result == null || result.size() < fetchSize || lastTableIndex < 0) {
partitionIndex++;
lastTableIndex = Integer.MAX_VALUE;
result = null;
if (partitionIndex == getPartitionCount()) {
return false;
}
}
result = fetch();
if (result != null && result.size() > 0) {
index = 0;
return true;
}
}
return false;
}
private Data getKey(int index) {
if (result != null) {
if (prefetchValues) {
Map.Entry<Data, Data> entry = (Map.Entry<Data, Data>) result.get(index);
return entry.getKey();
} else {
return (Data) result.get(index);
}
}
return null;
}
private V getValue(int index, K key) {
if (result != null) {
if (prefetchValues) {
Map.Entry<Data, Data> entry = (Map.Entry<Data, Data>) result.get(index);
return (V) toObject(entry.getValue());
} else {
return cache.get(key);
}
}
return null;
}
protected void ensureOpen() {
if (cache.isClosed()) {
throw new IllegalStateException("Cache operations can not be performed. The cache closed");
}
}
protected void setLastTableIndex(List response, int lastTableIndex) {
if (response != null && response.size() > 0) {
this.lastTableIndex = lastTableIndex;
}
}
protected int getPartitionCount() {
return partitionCount;
}
protected abstract List fetch();
protected abstract Data toData(Object obj);
protected abstract <T> T toObject(Object data);
}