/* * 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.igfs; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.cache.eviction.EvictableEntry; import org.apache.ignite.cache.eviction.EvictionPolicy; import org.apache.ignite.igfs.IgfsPath; import org.apache.ignite.internal.processors.cache.CacheEvictableEntryImpl; import org.apache.ignite.internal.processors.igfs.IgfsBlockKey; import org.apache.ignite.mxbean.IgniteMBeanAware; import org.jetbrains.annotations.Nullable; import org.jsr166.ConcurrentLinkedDeque8; import org.jsr166.ConcurrentLinkedDeque8.Node; import org.jsr166.LongAdder8; /** * IGFS eviction policy which evicts particular blocks. */ public class IgfsPerBlockLruEvictionPolicy implements EvictionPolicy<IgfsBlockKey, byte[]>, IgniteMBeanAware, Externalizable { /** */ private static final long serialVersionUID = 0L; /** Maximum size. When reached, eviction begins. */ private volatile long maxSize; /** Maximum amount of blocks. When reached, eviction begins. */ private volatile int maxBlocks; /** Collection of regex for paths which must not be evicted. */ private volatile Collection<String> excludePaths; /** Exclusion patterns. */ private volatile Collection<Pattern> excludePatterns; /** Whether patterns must be recompiled during the next call. */ private final AtomicBoolean excludeRecompile = new AtomicBoolean(true); /** Queue. */ private final ConcurrentLinkedDeque8<EvictableEntry<IgfsBlockKey, byte[]>> queue = new ConcurrentLinkedDeque8<>(); /** Current size of all enqueued blocks in bytes. */ private final LongAdder8 curSize = new LongAdder8(); /** * Default constructor. */ public IgfsPerBlockLruEvictionPolicy() { // No-op. } /** * Constructor. * * @param maxSize Maximum size. When reached, eviction begins. * @param maxBlocks Maximum amount of blocks. When reached, eviction begins. */ public IgfsPerBlockLruEvictionPolicy(long maxSize, int maxBlocks) { this(maxSize, maxBlocks, null); } /** * Constructor. * * @param maxSize Maximum size. When reached, eviction begins. * @param maxBlocks Maximum amount of blocks. When reached, eviction begins. * @param excludePaths Collection of regex for path which must not be evicted. */ public IgfsPerBlockLruEvictionPolicy(long maxSize, int maxBlocks, @Nullable Collection<String> excludePaths) { this.maxSize = maxSize; this.maxBlocks = maxBlocks; this.excludePaths = excludePaths; } /** {@inheritDoc} */ @Override public void onEntryAccessed(boolean rmv, EvictableEntry<IgfsBlockKey, byte[]> entry) { if (!rmv) { if (!entry.isCached()) return; if (touch(entry)) shrink(); } else { MetaEntry meta = entry.removeMeta(); if (meta != null && queue.unlinkx(meta.node())) changeSize(-meta.size()); } } /** * @param entry Entry to touch. * @return {@code True} if new node has been added to queue by this call. */ private boolean touch(EvictableEntry<IgfsBlockKey, byte[]> entry) { byte[] val = peek(entry); int blockSize = val != null ? val.length : 0; MetaEntry meta = entry.meta(); // Entry has not been enqueued yet. if (meta == null) { while (true) { Node<EvictableEntry<IgfsBlockKey, byte[]>> node = queue.offerLastx(entry); meta = new MetaEntry(node, blockSize); if (entry.putMetaIfAbsent(meta) != null) { // Was concurrently added, need to clear it from queue. queue.unlinkx(node); // Queue has not been changed. return false; } else if (node.item() != null) { if (!entry.isCached()) { // Was concurrently evicted, need to clear it from queue. queue.unlinkx(node); return false; } // Increment current size. changeSize(blockSize); return true; } // If node was unlinked by concurrent shrink() call, we must repeat the whole cycle. else if (!entry.removeMeta(node)) return false; } } else { int oldBlockSize = meta.size(); Node<EvictableEntry<IgfsBlockKey, byte[]>> node = meta.node(); if (queue.unlinkx(node)) { // Move node to tail. Node<EvictableEntry<IgfsBlockKey, byte[]>> newNode = queue.offerLastx(entry); int delta = blockSize - oldBlockSize; if (!entry.replaceMeta(meta, new MetaEntry(newNode, blockSize))) { // Was concurrently added, need to clear it from queue. if (queue.unlinkx(newNode)) delta -= blockSize; } if (delta != 0) { changeSize(delta); if (delta > 0) // Total size increased, so shrinking could be needed. return true; } } } // Entry is already in queue. return false; } /** * @param entry Entry. * @return Peeked value. */ @Nullable private byte[] peek(EvictableEntry<IgfsBlockKey, byte[]> entry) { return (byte[])((CacheEvictableEntryImpl)entry).peek(); } /** * Shrinks queue to maximum allowed size. */ private void shrink() { long maxSize = this.maxSize; int maxBlocks = this.maxBlocks; int cnt = queue.sizex(); for (int i = 0; i < cnt && (maxBlocks > 0 && queue.sizex() > maxBlocks || maxSize > 0 && curSize.longValue() > maxSize); i++) { EvictableEntry<IgfsBlockKey, byte[]> entry = queue.poll(); if (entry == null) break; // Queue is empty. byte[] val = peek(entry); if (val != null) changeSize(-val.length); // Change current size as we polled entry from the queue. if (!entry.evict()) { // Reorder entries which we failed to evict. entry.removeMeta(); touch(entry); } } } /** * Change current size. * * @param delta Delta in bytes. */ private void changeSize(int delta) { if (delta != 0) curSize.add(delta); } /** * Gets maximum allowed size of all blocks in bytes. * * @return Maximum allowed size of all blocks in bytes. */ public long getMaxSize() { return maxSize; } /** * Sets maximum allowed size of data in all blocks in bytes. * * @param maxSize Maximum allowed size of data in all blocks in bytes. * * @return {@code this} for chaining. */ public IgfsPerBlockLruEvictionPolicy setMaxSize(long maxSize) { this.maxSize = maxSize; return this; } /** * Gets maximum allowed amount of blocks. * * @return Maximum allowed amount of blocks. */ public int getMaxBlocks() { return maxBlocks; } /** * Sets maximum allowed amount of blocks. * * @param maxBlocks Maximum allowed amount of blocks. * * @return {@code this} for chaining. */ public IgfsPerBlockLruEvictionPolicy setMaxBlocks(int maxBlocks) { this.maxBlocks = maxBlocks; return this; } /** * Gets collection of regex for paths whose blocks must not be evicted. * * @return Collection of regex for paths whose blocks must not be evicted. */ public Collection<String> getExcludePaths() { return Collections.unmodifiableCollection(excludePaths); } /** * Sets collection of regex for paths whose blocks must not be evicted. * * @param excludePaths Collection of regex for paths whose blocks must not be evicted. * * @return {@code this} for chaining. */ public IgfsPerBlockLruEvictionPolicy setExcludePaths(@Nullable Collection<String> excludePaths) { this.excludePaths = excludePaths; excludeRecompile.set(true); return this; } /** * Gets current size of data in all blocks. * * @return Current size of data in all blocks. */ public long getCurrentSize() { return curSize.longValue(); } /** * Gets current amount of blocks. * * @return Current amount of blocks. */ public int getCurrentBlocks() { return queue.size(); } /** {@inheritDoc} */ @Override public Object getMBean() { return new IgfsPerBlockLruEvictionPolicyMXBeanImpl(); } /** {@inheritDoc} */ @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeLong(maxSize); out.writeInt(maxBlocks); out.writeObject(excludePaths); out.writeObject(excludePatterns); } /** {@inheritDoc} */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { maxSize = in.readLong(); maxBlocks = in.readInt(); excludePaths = (Collection<String>)in.readObject(); excludePatterns = (Collection<Pattern>)in.readObject(); } /** * Check whether provided path must be excluded from evictions. * * @param path Path. * @return {@code True} in case non block of related file must be excluded. * @throws IgniteCheckedException In case of faulty patterns. */ public boolean exclude(IgfsPath path) throws IgniteCheckedException { assert path != null; Collection<Pattern> excludePatterns0; if (excludeRecompile.compareAndSet(true, false)) { // Recompile. Collection<String> excludePaths0 = excludePaths; if (excludePaths0 != null) { excludePatterns0 = new HashSet<>(excludePaths0.size(), 1.0f); for (String excludePath : excludePaths0) { try { excludePatterns0.add(Pattern.compile(excludePath)); } catch (PatternSyntaxException ignore) { throw new IgniteCheckedException("Invalid regex pattern: " + excludePath); } } excludePatterns = excludePatterns0; } else excludePatterns0 = excludePatterns = null; } else excludePatterns0 = excludePatterns; if (excludePatterns0 != null) { String pathStr = path.toString(); for (Pattern pattern : excludePatterns0) { if (pattern.matcher(pathStr).matches()) return true; } } return false; } /** * Meta entry. */ private static class MetaEntry { /** Queue node. */ private final Node<EvictableEntry<IgfsBlockKey, byte[]>> node; /** Data size. */ private final int size; /** * Constructor. * * @param node Queue node. * @param size Data size. */ private MetaEntry(Node<EvictableEntry<IgfsBlockKey, byte[]>> node, int size) { assert node != null; assert size >= 0; this.node = node; this.size = size; } /** * @return Queue node. */ private Node<EvictableEntry<IgfsBlockKey, byte[]>> node() { return node; } /** * @return Data size. */ private int size() { return size; } } /** * MBean implementation for IgfsPerBlockLruEvictionPolicy. */ private class IgfsPerBlockLruEvictionPolicyMXBeanImpl implements IgfsPerBlockLruEvictionPolicyMXBean { /** {@inheritDoc} */ @Override public long getMaxSize() { return IgfsPerBlockLruEvictionPolicy.this.getMaxSize(); } /** {@inheritDoc} */ @Override public void setMaxSize(long maxSize) { IgfsPerBlockLruEvictionPolicy.this.setMaxSize(maxSize); } /** {@inheritDoc} */ @Override public int getMaxBlocks() { return IgfsPerBlockLruEvictionPolicy.this.getMaxBlocks(); } /** {@inheritDoc} */ @Override public void setMaxBlocks(int maxBlocks) { IgfsPerBlockLruEvictionPolicy.this.setMaxBlocks(maxBlocks); } /** {@inheritDoc} */ @Nullable @Override public Collection<String> getExcludePaths() { return IgfsPerBlockLruEvictionPolicy.this.getExcludePaths(); } /** {@inheritDoc} */ @Override public void setExcludePaths(@Nullable Collection<String> excludePaths) { IgfsPerBlockLruEvictionPolicy.this.setExcludePaths(excludePaths); } /** {@inheritDoc} */ @Override public long getCurrentSize() { return IgfsPerBlockLruEvictionPolicy.this.getCurrentSize(); } /** {@inheritDoc} */ @Override public int getCurrentBlocks() { return IgfsPerBlockLruEvictionPolicy.this.getCurrentBlocks(); } } }