/*
* 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.ignite.cache.eviction.sorted;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.ignite.cache.eviction.AbstractEvictionPolicy;
import org.apache.ignite.cache.eviction.EvictableEntry;
import org.apache.ignite.internal.util.GridConcurrentSkipListSet;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.mxbean.IgniteMBeanAware;
import org.jetbrains.annotations.Nullable;
import org.jsr166.LongAdder8;
import static java.lang.Math.abs;
import static org.apache.ignite.configuration.CacheConfiguration.DFLT_CACHE_SIZE;
/**
* Cache eviction policy which will select the minimum cache entry for eviction.
* <p>
* The eviction starts in the following cases:
* <ul>
* <li>The cache size becomes {@code batchSize} elements greater than the maximum size.</li>
* <li>
* The size of cache entries in bytes becomes greater than the maximum memory size.
* The size of cache entry calculates as sum of key size and value size.
* </li>
* </ul>
* <b>Note:</b>Batch eviction is enabled only if maximum memory limit isn't set ({@code maxMemSize == 0}).
* {@code batchSize} elements will be evicted in this case. The default {@code batchSize} value is {@code 1}.
* <p>
* Entries comparison based on {@link Comparator} instance if provided.
* Default {@code Comparator} behaviour is use cache entries keys for comparison that imposes a requirement for keys
* to implement {@link Comparable} interface.
* <p>
* User defined comparator should implement {@link Serializable} interface.
*/
public class SortedEvictionPolicy<K, V> extends AbstractEvictionPolicy<K, V> implements IgniteMBeanAware {
/** */
private static final long serialVersionUID = 0L;
/** Comparator. */
private Comparator<Holder<K, V>> comp;
/** Order. */
private final AtomicLong orderCnt = new AtomicLong();
/** Backed sorted queue. */
private final GridConcurrentSkipListSetEx<K, V> set;
/**
* Constructs sorted eviction policy with all defaults.
*/
public SortedEvictionPolicy() {
this(DFLT_CACHE_SIZE, null);
}
/**
* Constructs sorted eviction policy with maximum size.
*
* @param max Maximum allowed size of cache before entry will start getting evicted.
*/
public SortedEvictionPolicy(int max) {
this(max, null);
}
/**
* Constructs sorted eviction policy with given maximum size and given entry comparator.
*
* @param max Maximum allowed size of cache before entry will start getting evicted.
* @param comp Entries comparator.
*/
public SortedEvictionPolicy(int max, @Nullable Comparator<EvictableEntry<K, V>> comp) {
this(max, 1, comp);
}
/**
* Constructs sorted eviction policy with given maximum size, eviction batch size and entries comparator.
*
* @param max Maximum allowed size of cache before entry will start getting evicted.
* @param batchSize Batch size.
* @param comp Entries comparator.
*/
public SortedEvictionPolicy(int max, int batchSize, @Nullable Comparator<EvictableEntry<K, V>> comp) {
setMaxSize(max);
setBatchSize(batchSize);
this.comp = comp == null ? new DefaultHolderComparator<K, V>() : new HolderComparator<>(comp);
this.set = new GridConcurrentSkipListSetEx<>(this.comp);
}
/**
* Constructs sorted eviction policy with given maximum size and given entry comparator.
*
* @param comp Entries comparator.
*/
public SortedEvictionPolicy(@Nullable Comparator<EvictableEntry<K, V>> comp) {
this.comp = comp == null ? new DefaultHolderComparator<K, V>() : new HolderComparator<>(comp);
this.set = new GridConcurrentSkipListSetEx<>(this.comp);
}
/** {@inheritDoc} */
@Override public SortedEvictionPolicy<K, V> setMaxMemorySize(long maxMemSize) {
super.setMaxMemorySize(maxMemSize);
return this;
}
/** {@inheritDoc} */
@Override public SortedEvictionPolicy<K, V> setMaxSize(int max) {
super.setMaxSize(max);
return this;
}
/** {@inheritDoc} */
@Override public SortedEvictionPolicy<K, V> setBatchSize(int batchSize) {
super.setBatchSize(batchSize);
return this;
}
/**
* Gets read-only view of backed queue in proper order.
*
* @return Read-only view of backed queue.
*/
public Collection<EvictableEntry<K, V>> queue() {
Set<EvictableEntry<K, V>> cp = new LinkedHashSet<>();
for (Holder<K, V> holder : set)
cp.add(holder.entry);
return Collections.unmodifiableCollection(cp);
}
/**
* @param entry Entry to touch.
* @return {@code True} if backed queue has been changed by this call.
*/
@Override protected boolean touch(EvictableEntry<K, V> entry) {
Holder<K, V> holder = entry.meta();
// Entry has not been added yet to backed queue.
if (holder == null) {
while (true) {
holder = new Holder<>(entry, orderCnt.incrementAndGet());
if (entry.putMetaIfAbsent(holder) != null)
return false; // Set has not been changed.
set.add(holder);
if (holder.order > 0) {
if (!entry.isCached()) {
// Was concurrently evicted, need to remove it from queue.
removeMeta(holder);
return false;
}
memSize.add(entry.size());
return true;
}
// If holder was removed by concurrent shrink() call, we must repeat the whole cycle.
else if (!entry.removeMeta(holder))
return false;
}
}
// Entry is already in queue.
return false;
}
/** {@inheritDoc} */
@Override public int getCurrentSize() {
return set.sizex();
}
/**
* Tries to remove one item from queue.
*
* @return number of bytes that was free. {@code -1} if queue is empty.
*/
@Override protected int shrink0() {
Holder<K, V> h = set.pollFirst();
if (h == null)
return -1;
int size = 0;
EvictableEntry<K, V> entry = h.entry;
assert entry != null;
if (h.order > 0 && entry.removeMeta(h)) {
size = entry.size();
memSize.add(-size);
if (!entry.evict())
touch(entry);
}
return size;
}
/** {@inheritDoc} */
@Override public Object getMBean() {
return new SortedEvictionPolicyMBeanImpl();
}
/** {@inheritDoc} */
@Override public void writeExternal(ObjectOutput out) throws IOException {
super.writeExternal(out);
out.writeObject(comp);
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
super.readExternal(in);
comp = (Comparator<Holder<K, V>>)in.readObject();
}
/**
* Removes holder from backed queue and marks holder as removed.
*
* @param meta Holder.
*/
@SuppressWarnings("unchecked")
@Override protected boolean removeMeta(Object meta) {
Holder<K, V> holder = (Holder<K, V>)meta;
long order0 = holder.order;
if (order0 > 0)
holder.order = -order0;
return set.remove(holder);
}
/**
* Evictable entry holder.
*/
private static class Holder<K, V> {
/** Entry. */
private final EvictableEntry<K, V> entry;
/** Order needs for distinguishing keys that are equal. */
private volatile long order;
/**
* Constructs holder for given key.
*
* @param entry Entry.
* @param order Order.
*/
public Holder(EvictableEntry<K, V> entry, long order) {
assert order > 0;
this.entry = entry;
this.order = order;
}
/** {@inheritDoc} */
@Override public int hashCode() {
return entry.hashCode();
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || this.getClass() != obj.getClass())
return false;
Holder<K, V> h = (Holder<K, V>) obj;
return Objects.equals(entry, h.entry) && abs(order) == abs(h.order);
}
}
/**
* Evictable entries holder comparator. Wrapper for client's comparator.
*/
private static class HolderComparator<K, V> implements Comparator<Holder<K, V>>, Serializable {
/** */
private static final long serialVersionUID = 0L;
/** Keys comparator. */
private final Comparator<EvictableEntry<K, V>> comp;
/**
* @param comp Comparator.
*/
public HolderComparator(Comparator<EvictableEntry<K, V>> comp) {
A.notNull(comp, "comp");
this.comp = comp;
}
/** {@inheritDoc} */
@Override public int compare(Holder<K, V> h1, Holder<K, V> h2) {
if (h1 == h2)
return 0;
EvictableEntry<K, V> e1 = h1.entry;
EvictableEntry<K, V> e2 = h2.entry;
int cmp = comp.compare(e1, e2);
return cmp == 0 ? Long.compare(abs(h1.order), abs(h2.order)) : cmp;
}
}
/**
* Default comparator. Uses if comparator isn't provided by client.
* Compares only entry keys that should implements {@link Comparable} interface.
*/
private static class DefaultHolderComparator<K, V> implements Comparator<Holder<K, V>>, Serializable {
/** */
private static final long serialVersionUID = 0L;
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override public int compare(Holder<K, V> h1, Holder<K, V> h2) {
if (h1 == h2)
return 0;
EvictableEntry<K, V> e1 = h1.entry;
EvictableEntry<K, V> e2 = h2.entry;
int cmp = ((Comparable<K>)e1.getKey()).compareTo(e2.getKey());
return cmp == 0 ? Long.compare(abs(h1.order), abs(h2.order)) : cmp;
}
}
/**
* Provides additional method {@code #sizex()}. NOTE: Only the following methods supports this addition:
* <ul>
* <li>{@code #add()}</li>
* <li>{@code #remove()}</li>
* <li>{@code #pollFirst()}</li>
* <li>{@code #clone()}</li>
* <ul/>
*/
private static class GridConcurrentSkipListSetEx<K, V> extends GridConcurrentSkipListSet<Holder<K, V>> {
/** */
private static final long serialVersionUID = 0L;
/** Size. */
private final LongAdder8 size = new LongAdder8();
/**
* @param comp Comparator.
*/
public GridConcurrentSkipListSetEx(Comparator<? super Holder<K, V>> comp) {
super(comp);
}
/**
* @return Size based on performed operations.
*/
public int sizex() {
return size.intValue();
}
/** {@inheritDoc} */
@Override public boolean add(Holder<K, V> e) {
boolean res = super.add(e);
assert res;
size.increment();
return res;
}
/** {@inheritDoc} */
@Override public boolean remove(Object o) {
boolean res = super.remove(o);
if (res)
size.decrement();
return res;
}
/** {@inheritDoc} */
@Nullable @Override public Holder<K, V> pollFirst() {
Holder<K, V> e = super.pollFirst();
if (e != null)
size.decrement();
return e;
}
}
/**
* MBean implementation for SortedEvictionPolicy.
*/
private class SortedEvictionPolicyMBeanImpl implements SortedEvictionPolicyMBean {
/** {@inheritDoc} */
@Override public long getCurrentMemorySize() {
return SortedEvictionPolicy.this.getCurrentMemorySize();
}
/** {@inheritDoc} */
@Override public int getCurrentSize() {
return SortedEvictionPolicy.this.getCurrentSize();
}
/** {@inheritDoc} */
@Override public int getMaxSize() {
return SortedEvictionPolicy.this.getMaxSize();
}
/** {@inheritDoc} */
@Override public void setMaxSize(int max) {
SortedEvictionPolicy.this.setMaxSize(max);
}
/** {@inheritDoc} */
@Override public int getBatchSize() {
return SortedEvictionPolicy.this.getBatchSize();
}
/** {@inheritDoc} */
@Override public void setBatchSize(int batchSize) {
SortedEvictionPolicy.this.setBatchSize(batchSize);
}
/** {@inheritDoc} */
@Override public long getMaxMemorySize() {
return SortedEvictionPolicy.this.getMaxMemorySize();
}
/** {@inheritDoc} */
@Override public void setMaxMemorySize(long maxMemSize) {
SortedEvictionPolicy.this.setMaxMemorySize(maxMemSize);
}
}
}