/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.collections.timed; import com.jcwhatever.nucleus.Nucleus; import com.jcwhatever.nucleus.collections.wrap.IteratorWrapper; import com.jcwhatever.nucleus.managed.scheduler.IScheduledTask; import com.jcwhatever.nucleus.managed.scheduler.Scheduler; import com.jcwhatever.nucleus.mixins.IPluginOwned; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.Rand; import com.jcwhatever.nucleus.utils.TimeScale; import com.jcwhatever.nucleus.utils.observer.update.IUpdateSubscriber; import com.jcwhatever.nucleus.utils.observer.update.NamedUpdateAgents; import com.jcwhatever.nucleus.utils.performance.pool.IPoolElementFactory; import com.jcwhatever.nucleus.utils.performance.pool.SimpleConcurrentPool; import org.bukkit.plugin.Plugin; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; import java.util.WeakHashMap; /** * A hash set where each item has its own lifespan. When an items lifespan ends, * it is removed from the hash set. * * <p>If a duplicate item is added, the items lifespan is reset, in addition to normal * hash set operations.</p> * * <p>Subscribers that are added to track when an item expires or the collection is * empty will have a varying degree of resolution up to 10 ticks, meaning the subscriber * may be notified up to 10 ticks after an element expires (but not before).</p> * * <p>Getter operations cease to return an element within approximately 50 milliseconds * (1 tick) of expiring.</p> * * <p>Thread safe.</p> * * <p>The sets iterators must be used inside a synchronized block which locks the * set instance. Otherwise, a {@link java.lang.IllegalStateException} is thrown.</p> */ public class TimedHashSet<E> implements Set<E>, IPluginOwned { // The minimum interval the cleanup is allowed to run at. // Used to prevent cleanup from being run too often. private static final int MIN_CLEANUP_INTERVAL_MS = 50; // The interval the janitor runs at private static final int JANITOR_INTERVAL_TICKS = 10; // random initial delay interval for janitor, random to help spread out // task execution in relation to other scheduled tasks private static final int JANITOR_INITIAL_DELAY_TICKS = Rand.getInt(1, 9); private final static Map<TimedHashSet, Void> _instances = new WeakHashMap<>(10); private static volatile IScheduledTask _janitor; private final Plugin _plugin; private final Map<E, ExpireInfo> _expireMap; private final int _lifespan; // milliseconds private final TimeScale _timeScale; private final transient Object _sync; private transient long _nextCleanup; private final transient NamedUpdateAgents _agents = new NamedUpdateAgents(); private final transient List<Entry<E, ExpireInfo>> _cleanupList = new ArrayList<>(20); private final transient SimpleConcurrentPool<ExpireInfo> _expirePool; /** * Constructor. * * <p>Default lifespan is 20 ticks.</p> */ public TimedHashSet(Plugin plugin) { this(plugin, 10, 20, TimeScale.TICKS); } /** * Constructor. * * <p>Default lifespan is 20 ticks.</p> * * @param capacity The initial capacity. */ public TimedHashSet(Plugin plugin, int capacity) { this(plugin, capacity, 20, TimeScale.TICKS); } /** * Constructor. * * <p>Default lifespan is 20 ticks.</p> * * @param capacity The initial capacity. * @param defaultLifespan The default lifespan. */ public TimedHashSet(Plugin plugin, int capacity, int defaultLifespan) { this(plugin, capacity, defaultLifespan, TimeScale.TICKS); } /** * Constructor. * * @param capacity The initial capacity. * @param defaultLifespan The default lifespan. * @param timeScale The lifespan time scale. */ public TimedHashSet(Plugin plugin, int capacity, int defaultLifespan, TimeScale timeScale) { PreCon.notNull(plugin); PreCon.positiveNumber(defaultLifespan); PreCon.notNull(timeScale); _plugin = plugin; _sync = this; _lifespan = defaultLifespan * timeScale.getTimeFactor(); _timeScale = timeScale; _expireMap = new HashMap<>(capacity); _expirePool = new SimpleConcurrentPool<ExpireInfo>(capacity, new IPoolElementFactory<ExpireInfo>() { @Override public ExpireInfo create() { return new ExpireInfo(); } }); synchronized (_instances) { _instances.put(this, null); } startJanitor(); } /** * Determine if the set contains the specified item and * if present, reset the items lifespan using the time scale * specified in the constructor. * * @param item The item to check. * @param lifespan The new lifespan of the item. */ public boolean contains(E item, int lifespan) { return contains(item, lifespan, _timeScale); } /** * Determine if the set contains the specified item and * if present, reset the items lifespan using the specified * time scale. * * @param item The item to check. * @param lifespan The new lifespan of the item. * @param timeScale The time scale of the specified lifespan. */ public boolean contains(E item, int lifespan, TimeScale timeScale) { PreCon.notNull(item); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); synchronized (_sync) { ExpireInfo info = _expireMap.get(item); if (info == null) return false; if (info.isExpired()) { _expireMap.remove(item); onLifespanEnd(item); _expirePool.recycle(info); return false; } ExpireInfo expireInfo = _expirePool.retrieve(); assert expireInfo != null; _expireMap.put(item, expireInfo.set(lifespan, timeScale)); return true; } } /** * Add an item to the hash set with the specified lifespan * using the time scale specified in the constructor. * * <p>If the item is already present, the lifespan is reset * using the new lifespan value.</p> * * @param item The item to add. * @param lifespan The lifespan of the item. * * @return True if added. */ public boolean add(E item, int lifespan) { return add(item, lifespan, _timeScale); } /** * Add an item to the hash set with the specified lifespan * using the time scale specified. * * <p>If the item is already present, the lifespan is reset * using the new lifespan value and the item is replaced with * the new value.</p> * * @param item The item to add. * @param lifespan The lifespan of the item. * @param timeScale The time scale of the specified lifespan. * * @return True if added. */ public boolean add(E item, int lifespan, TimeScale timeScale) { PreCon.notNull(item); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); synchronized (_sync) { ExpireInfo expireInfo = _expirePool.retrieve(); assert expireInfo != null; _expireMap.put(item, expireInfo.set(lifespan, timeScale)); return true; } } /** * Add items from a collection using the specified lifespan in * the time scale specified in the constructor. * * <p>Any item that is already present will have its lifespan reset * using the specified lifespan value and the item is replaced with * the new element.</p> * * @param collection The collection to add. * @param lifespan The lifespan of the item. * * @return True if the internal collection was modified. */ public boolean addAll(Collection<? extends E> collection, int lifespan) { return addAll(collection, lifespan, _timeScale); } /** * Add items from a collection using the specified lifespan in * the time scale specified. * * <p>Any item that is already present will have its lifespan reset * using the specified lifespan value and the item is replaced with * the new element.</p> * * @param collection The collection to add. * @param lifespan The lifespan of the item. * @param timeScale The time scale of the specified lifespan. */ public boolean addAll(Collection<? extends E> collection, int lifespan, TimeScale timeScale) { PreCon.notNull(collection); PreCon.positiveNumber(lifespan); synchronized (_sync) { for (E item : collection) { ExpireInfo expireInfo = _expirePool.retrieve(); assert expireInfo != null; _expireMap.put(item, expireInfo.set(lifespan, timeScale)); } } return true; } /** * Set the maximum size of the internal object pool used * for pooling internal instances. * * @param poolSize The maximum pool size. -1 for "infinite". * * @return Self for chaining. */ public TimedHashSet<E> setMaxPoolSize(int poolSize) { _expirePool.setMaxSize(poolSize); return this; } /** * Get the maximum size of the internal object pool used * for pooling internal instances. */ public int getMaxPoolSize() { return _expirePool.maxSize(); } /** * Register a subscriber to be notified when an elements lifespan * ends. * * @param subscriber The subscriber to register. * * @return Self for chaining. */ public TimedHashSet<E> onLifespanEnd(IUpdateSubscriber<E> subscriber) { PreCon.notNull(subscriber); _agents.getAgent("onLifespanEnd").addSubscriber(subscriber); return this; } /** * Register a subscriber to be notified when the collection is empty * due to elements expiring. * * @param subscriber The subscriber to register. * * @return Self for chaining. */ public TimedHashSet<E> onEmpty(IUpdateSubscriber<TimedHashSet<E>> subscriber) { PreCon.notNull(subscriber); _agents.getAgent("onEmpty").addSubscriber(subscriber); return this; } @Override public Plugin getPlugin() { return _plugin; } @Override public int size() { synchronized (_sync) { cleanup(); return _expireMap.size(); } } @Override public boolean isEmpty() { synchronized (_sync) { cleanup(); return _expireMap.isEmpty(); } } @Override public boolean contains(Object o) { PreCon.notNull(o); synchronized (_sync) { //noinspection SuspiciousMethodCalls ExpireInfo info = _expireMap.get(o); if (info == null) return false; if (info.isExpired()) { //noinspection SuspiciousMethodCalls _expireMap.remove(o); _expirePool.recycle(info); return false; } return true; } } @Override public Iterator<E> iterator() { return new Itr(); } @Override public Object[] toArray() { synchronized (_sync) { cleanup(); return _expireMap.keySet().toArray(); } } @Override public <T> T[] toArray(T[] a) { PreCon.notNull(a); synchronized (_sync) { cleanup(); //noinspection SuspiciousToArrayCall return _expireMap.keySet().toArray(a); } } @Override public boolean add(E item) { PreCon.notNull(item); return add(item, _lifespan, TimeScale.MILLISECONDS); } @Override public boolean addAll(Collection<? extends E> collection) { PreCon.notNull(collection); return addAll(collection, _lifespan, TimeScale.MILLISECONDS); } @Override public boolean retainAll(Collection<?> c) { PreCon.notNull(c); synchronized (_sync) { return _expireMap.keySet().retainAll(c); } } @Override public void clear() { synchronized (_sync) { for (Entry<E, ExpireInfo> entry : _expireMap.entrySet()) { _expirePool.recycle(entry.getValue()); } _expireMap.clear(); } } @Override public boolean remove(Object item) { PreCon.notNull(item); synchronized (_sync) { ExpireInfo info = _expireMap.remove(item); if (info == null) return false; if (info.isExpired()) { _expirePool.recycle(info); return false; } } return true; } @Override public boolean containsAll(Collection<?> c) { PreCon.notNull(c); synchronized (_sync) { cleanup(); return _expireMap.keySet().containsAll(c); } } @Override public boolean removeAll(Collection<?> collection) { PreCon.notNull(collection); boolean isChanged = false; synchronized (_sync) { for (Object item : collection) { //noinspection SuspiciousMethodCalls ExpireInfo info = _expireMap.remove(item); if (info == null) continue; if (!info.isExpired()) isChanged = true; _expirePool.recycle(info); } return isChanged; } } private void onLifespanEnd(E item) { _agents.update("onLifespanEnd", item); if (_expireMap.isEmpty()) { _agents.update("onEmpty", this); } } private void cleanup() { if (_expireMap.isEmpty()) return; // prevent cleanup from running too often if (_nextCleanup > System.currentTimeMillis()) return; _nextCleanup = System.currentTimeMillis() + MIN_CLEANUP_INTERVAL_MS; _cleanupList.addAll(_expireMap.entrySet()); for (Entry<E, ExpireInfo> entry : _cleanupList) { if (!entry.getValue().isExpired()) continue; _expireMap.remove(entry.getKey()); onLifespanEnd(entry.getKey()); _expirePool.recycle(entry.getValue()); } _cleanupList.clear(); } private void startJanitor() { if (_janitor != null) return; _janitor = Scheduler.runTaskRepeatAsync(Nucleus.getPlugin(), JANITOR_INITIAL_DELAY_TICKS, JANITOR_INTERVAL_TICKS, new Runnable() { List<TimedHashSet> sets = new ArrayList<>(15); volatile boolean isRunning; @Override public void run() { if (isRunning) return; isRunning = true; synchronized (_instances) { sets.addAll(_instances.keySet()); } for (TimedHashSet set : sets) { // remove instance if owning plugin is not enabled if (!set.getPlugin().isEnabled()) { synchronized (_instances) { _instances.remove(set); } continue; } // cleanup instance synchronized (set._sync) { set.cleanup(); } } sets.clear(); isRunning = false; } }); } private static final class ExpireInfo { long expires; ExpireInfo set(long lifespan, TimeScale timeScale) { expires = System.currentTimeMillis() + (lifespan * timeScale.getTimeFactor()); return this; } boolean isExpired() { return System.currentTimeMillis() >= expires; } } private final class Itr implements Iterator<E> { Iterator<Entry<E, ExpireInfo>> iterator = _expireMap.entrySet().iterator(); Entry<E, ExpireInfo> peek; boolean invokedHasNext; boolean invokedNext; @Override public boolean hasNext() { IteratorWrapper.assertIteratorLock(_sync); invokedHasNext = true; if (!iterator.hasNext()) return false; while (iterator.hasNext()) { peek = iterator.next(); if (peek.getValue().isExpired()) { iterator.remove(); } else { return true; } } return false; } @Override public E next() { IteratorWrapper.assertIteratorLock(_sync); if (!invokedHasNext) throw new IllegalStateException("Cannot invoke 'next' before invoking 'hasNext'."); invokedHasNext = false; invokedNext = true; if (peek == null) hasNext(); if (peek == null) throw new NoSuchElementException(); Entry<E, ExpireInfo> n = peek; peek = null; return n.getKey(); } @Override public void remove() { IteratorWrapper.assertIteratorLock(_sync); if (!invokedNext) throw new IllegalStateException("Cannot invoke 'remove' before invoking 'next'"); iterator.remove(); } } }