/* * 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.util.scheduler; import com.hazelcast.spi.TaskScheduler; import com.hazelcast.util.Clock; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * Schedule execution of an entry for seconds later. * This is similar to a scheduled executor service, but instead of scheduling * a execution for a specific millisecond, this service will * schedule it with second proximity. For example, if delayMillis is 600 ms, * then the entry will be scheduled to execute in 1 second. If delayMillis is 2400, * then the entry will be scheduled to execute in 3 seconds. Therefore, delayMillis is * ceil-ed to the next second. It gives up exact time scheduling to gain * the power of: * a) bulk execution of all operations within the same second * or * b) being able to reschedule (postpone) execution. * * @param <K> entry key type * @param <V> entry value type */ final class SecondsBasedEntryTaskScheduler<K, V> implements EntryTaskScheduler<K, V> { public static final int INITIAL_CAPACITY = 10; public static final double FACTOR = 1000d; private static final long INITIAL_TIME_MILLIS = Clock.currentTimeMillis(); private static final Comparator<ScheduledEntry> SCHEDULED_ENTRIES_COMPARATOR = new Comparator<ScheduledEntry>() { @Override public int compare(ScheduledEntry o1, ScheduledEntry o2) { if (o1.getScheduleId() > o2.getScheduleId()) { return 1; } else if (o1.getScheduleId() < o2.getScheduleId()) { return -1; } return 0; } }; /** Map of keys to duration between this class being loaded and the time the key is scheduled */ private final Map<Object, Integer> secondsOfKeys = new HashMap<Object, Integer>(1000); /** Map from duration (see {@link #findRelativeSecond(long)} to scheduled key to scheduled entry map. */ private final Map<Integer, Map<Object, ScheduledEntry<K, V>>> scheduledEntries = new HashMap<Integer, Map<Object, ScheduledEntry<K, V>>>(1000); private final Map<Integer, ScheduledFuture> scheduledTaskMap = new HashMap<Integer, ScheduledFuture>(1000); private final AtomicLong uniqueIdGenerator = new AtomicLong(); private final Object mutex = new Object(); private final TaskScheduler taskScheduler; private final ScheduledEntryProcessor<K, V> entryProcessor; private final ScheduleType scheduleType; SecondsBasedEntryTaskScheduler(TaskScheduler taskScheduler, ScheduledEntryProcessor<K, V> entryProcessor, ScheduleType scheduleType) { this.taskScheduler = taskScheduler; this.entryProcessor = entryProcessor; this.scheduleType = scheduleType; } @Override public boolean schedule(long delayMillis, K key, V value) { if (scheduleType.equals(ScheduleType.POSTPONE)) { return schedulePostponeEntry(delayMillis, key, value); } else if (scheduleType.equals(ScheduleType.FOR_EACH)) { return scheduleEntry(delayMillis, key, value); } throw new RuntimeException("Undefined schedule type."); } private boolean schedulePostponeEntry(long delayMillis, K key, V value) { int delaySeconds = ceilToSecond(delayMillis); Integer newSecond = findRelativeSecond(delayMillis); synchronized (mutex) { Integer existingSecond = secondsOfKeys.put(key, newSecond); if (existingSecond != null) { if (existingSecond.equals(newSecond)) { return false; } removeKeyFromSecond(key, existingSecond); } long id = uniqueIdGenerator.incrementAndGet(); ScheduledEntry<K, V> scheduledEntry = new ScheduledEntry<K, V>(key, value, delayMillis, delaySeconds, id); doSchedule(key, scheduledEntry, newSecond); } return true; } private boolean scheduleEntry(long delayMillis, K key, V value) { int delaySeconds = ceilToSecond(delayMillis); Integer newSecond = findRelativeSecond(delayMillis); synchronized (mutex) { long id = uniqueIdGenerator.incrementAndGet(); Object compositeKey = new CompositeKey(key, id); secondsOfKeys.put(compositeKey, newSecond); ScheduledEntry<K, V> scheduledEntry = new ScheduledEntry<K, V>(key, value, delayMillis, delaySeconds, id); doSchedule(compositeKey, scheduledEntry, newSecond); } return true; } private void doSchedule(Object mapKey, ScheduledEntry<K, V> entry, Integer second) { Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.get(second); boolean shouldSchedule = false; if (entries == null) { entries = new HashMap<Object, ScheduledEntry<K, V>>(INITIAL_CAPACITY); scheduledEntries.put(second, entries); // we created the second // so we will schedule its execution shouldSchedule = true; } entries.put(mapKey, entry); if (shouldSchedule) { schedule(second, entry.getActualDelaySeconds()); } } @Override public ScheduledEntry<K, V> cancel(K key) { synchronized (mutex) { if (scheduleType.equals(ScheduleType.FOR_EACH)) { return cancelByCompositeKey(key); } Integer second = secondsOfKeys.remove(key); if (second == null) { return null; } Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.get(second); if (entries == null) { return null; } return cancelAndCleanUpIfEmpty(second, entries, key); } } @Override public int cancelIfExists(K key, V value) { synchronized (mutex) { ScheduledEntry<K, V> scheduledEntry = new ScheduledEntry<K, V>(key, value, 0, 0, 0); if (scheduleType.equals(ScheduleType.FOR_EACH)) { return cancelByCompositeKey(key, scheduledEntry); } Integer second = secondsOfKeys.remove(key); if (second == null) { return 0; } Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.get(second); if (entries == null) { return 0; } return cancelAndCleanUpIfEmpty(second, entries, key, scheduledEntry) ? 1 : 0; } } // in the case of composite keys this method will return only one scheduled entry with no ordering guarantee @Override public ScheduledEntry<K, V> get(K key) { synchronized (mutex) { if (scheduleType.equals(ScheduleType.FOR_EACH)) { return getByCompositeKey(key); } Integer second = secondsOfKeys.get(key); if (second != null) { Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.get(second); if (entries != null) { return entries.get(key); } } return null; } } private ScheduledEntry<K, V> cancelByCompositeKey(K key) { Set<CompositeKey> candidateKeys = getCompositeKeys(key); ScheduledEntry<K, V> result = null; for (CompositeKey compositeKey : candidateKeys) { Integer second = secondsOfKeys.remove(compositeKey); if (second == null) { continue; } Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.get(second); if (entries == null) { continue; } result = cancelAndCleanUpIfEmpty(second, entries, compositeKey); } return result; } private int cancelByCompositeKey(K key, ScheduledEntry<K, V> entryToRemove) { int cancelled = 0; for (CompositeKey compositeKey : getCompositeKeys(key)) { Integer second = secondsOfKeys.remove(compositeKey); if (second == null) { continue; } Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.get(second); if (entries == null) { continue; } if (cancelAndCleanUpIfEmpty(second, entries, compositeKey, entryToRemove)) { cancelled++; } } return cancelled; } /** Return all composite keys with the given {@code key} */ private Set<CompositeKey> getCompositeKeys(K key) { Set<CompositeKey> candidateKeys = new HashSet<CompositeKey>(); for (Object keyObj : secondsOfKeys.keySet()) { CompositeKey compositeKey = (CompositeKey) keyObj; if (compositeKey.getKey().equals(key)) { candidateKeys.add(compositeKey); } } return candidateKeys; } /** Returns one scheduled entry for the given {@code key} with no guaranteed ordering */ public ScheduledEntry<K, V> getByCompositeKey(K key) { Set<CompositeKey> candidateKeys = getCompositeKeys(key); ScheduledEntry<K, V> result = null; for (CompositeKey compositeKey : candidateKeys) { Integer second = secondsOfKeys.get(compositeKey); if (second != null) { Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.get(second); if (entries != null) { result = entries.get(compositeKey); } } } return result; } private void removeKeyFromSecond(Object key, Integer existingSecond) { Map<Object, ScheduledEntry<K, V>> scheduledKeys = scheduledEntries.get(existingSecond); if (scheduledKeys != null) { cancelAndCleanUpIfEmpty(existingSecond, scheduledKeys, key); } } /** * Removes the entry from being scheduled to be evicted. * <p/> * Cleans up parent container (second -> entries map) if it doesn't hold anymore items this second. * <p/> * Cancels associated scheduler (second -> scheduler map ) if there are no more items to remove for this second. * <p/> * Returns associated scheduled entry. * * @param second second at which this entry was scheduled to be evicted * @param entries entries which were already scheduled to be evicted for this second * @param key entry key * @return associated scheduled entry */ private ScheduledEntry<K, V> cancelAndCleanUpIfEmpty(Integer second, Map<Object, ScheduledEntry<K, V>> entries, Object key) { ScheduledEntry<K, V> result = entries.remove(key); cleanUpScheduledFuturesIfEmpty(second, entries); return result; } /** * Removes the entry if it exists from being scheduled to be evicted. * <p/> * Cleans up parent container (second -> entries map) if it doesn't hold anymore items this second. * <p/> * Cancels associated scheduler (second -> scheduler map ) if there are no more items to remove for this second. * <p/> * Returns associated scheduled entry. * * @param second second at which this entry was scheduled to be evicted * @param entries entries which were already scheduled to be evicted for this second * @param key entry key * @param entryToRemove entry value that is expected to exist in the map * @return true if entryToRemove exists in the map and removed */ private boolean cancelAndCleanUpIfEmpty(Integer second, Map<Object, ScheduledEntry<K, V>> entries, Object key, ScheduledEntry<K, V> entryToRemove) { ScheduledEntry<K, V> entry = entries.get(key); if (entry == null || !entry.equals(entryToRemove)) { return false; } entries.remove(key); cleanUpScheduledFuturesIfEmpty(second, entries); return true; } /** * Cancels the scheduled future and removes the entries map for the given second If no entries are left * <p/> * Cleans up parent container (second -> entries map) if it doesn't hold anymore items this second. * <p/> * Cancels associated scheduler (second -> scheduler map ) if there are no more items to remove for this second. * * @param second second at which this entry was scheduled to be evicted * @param entries entries which were already scheduled to be evicted for this second */ private void cleanUpScheduledFuturesIfEmpty(Integer second, Map<Object, ScheduledEntry<K, V>> entries) { if (entries.isEmpty()) { scheduledEntries.remove(second); ScheduledFuture removedFeature = scheduledTaskMap.remove(second); if (removedFeature != null) { removedFeature.cancel(false); } } } private void schedule(Integer second, int delaySeconds) { EntryProcessorExecutor command = new EntryProcessorExecutor(second); ScheduledFuture scheduledFuture = taskScheduler.schedule(command, delaySeconds, TimeUnit.SECONDS); scheduledTaskMap.put(second, scheduledFuture); } public void cancelAll() { synchronized (mutex) { secondsOfKeys.clear(); scheduledEntries.clear(); for (ScheduledFuture task : scheduledTaskMap.values()) { task.cancel(false); } scheduledTaskMap.clear(); } } @Override public String toString() { return "EntryTaskScheduler{" + "secondsOfKeys=" + secondsOfKeys.size() + ", scheduledEntries [" + scheduledEntries.size() + "] =" + scheduledEntries.keySet() + '}'; } // just for testing int size() { synchronized (mutex) { return secondsOfKeys.size(); } } /** Returns the duration in seconds between the time this class was loaded and now+{@code delayMillis} */ // package private for testing static int findRelativeSecond(long delayMillis) { long now = Clock.currentTimeMillis(); long d = (now + delayMillis - INITIAL_TIME_MILLIS); return ceilToSecond(d); } private static int ceilToSecond(long delayMillis) { return (int) Math.ceil(delayMillis / FACTOR); } private static <K, V> List<ScheduledEntry<K, V>> sortForEntryProcessing(List<ScheduledEntry<K, V>> coll) { if (coll == null || coll.isEmpty()) { return Collections.emptyList(); } Collections.sort(coll, SCHEDULED_ENTRIES_COMPARATOR); return coll; } private final class EntryProcessorExecutor implements Runnable { private final Integer second; private EntryProcessorExecutor(Integer second) { this.second = second; } @Override public void run() { List<ScheduledEntry<K, V>> values; synchronized (mutex) { scheduledTaskMap.remove(second); Map<Object, ScheduledEntry<K, V>> entries = scheduledEntries.remove(second); if (entries == null || entries.isEmpty()) { return; } values = new ArrayList<ScheduledEntry<K, V>>(entries.size()); for (Map.Entry<Object, ScheduledEntry<K, V>> entry : entries.entrySet()) { Integer removed = secondsOfKeys.remove(entry.getKey()); if (removed != null) { values.add(entry.getValue()); } } } //sort entries asc by schedule times and send to processor. entryProcessor.process(SecondsBasedEntryTaskScheduler.this, sortForEntryProcessing(values)); } } }