package com.webonise.tomcat8.redisession.redisclient; import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.function.*; /** * An iterator over a {@code SCAN} starting at the given cursor. */ public class ScanSpliterator implements Spliterator<String> { /** * The initial cursor to perofrm a scan. */ public static final String STARTING_CURSOR = "0"; private volatile ForkJoinTask<ScanSpliterator> next; // NB: synchronize(this) if you touch this.next private final String[] results; private final AtomicInteger idx = new AtomicInteger(0); /** * Construct the iterator to start at the cursor with no match results. * * @param redisClient The client to use; may not be {@code null}. * @param cursor The cursor to start at; may not be {@code null}. */ public ScanSpliterator(Redis redisClient, String cursor) throws Exception { this(redisClient, cursor, null); } /** * Construct the iterator to start at the cursor with no match results. * * @param redisClient The client to use; may not be {@code null}. * @param cursor The cursor to start at; may not be {@code null}. * @param params The parameters; {@code null} means no parameters. */ public ScanSpliterator(Redis redisClient, String cursor, ScanParams params) throws Exception { Objects.requireNonNull(redisClient, "Client for Redis"); Objects.requireNonNull(cursor, "The cursor to start from (you probably meanto use STARTING_CURSOR)"); ScanResult<String> scanResult = redisClient.withRedis(jedis -> { if (params == null) { return jedis.scan(cursor); } else { return jedis.scan(cursor, params); } }); results = scanResult.getResult().toArray(new String[scanResult.getResult().size()]); if (scanResult.getStringCursor().equals("0")) { this.next = null; } else { this.next = ForkJoinPool.commonPool().submit(() -> { return new ScanSpliterator(redisClient, scanResult.getStringCursor(), params); }); } } /** * If a remaining element exists, performs the given action on it, * returning {@code true}; else returns {@code false}. If this * Spliterator is {@link #ORDERED} the action is performed on the * next element in encounter order. Exceptions thrown by the * action are relayed to the caller. * * @param action The action * @return {@code false} if no remaining elements existed * upon entry to this method, else {@code true}. * @throws NullPointerException if the specified action is null */ @Override public boolean tryAdvance(Consumer<? super String> action) { if (action == null) throw new NullPointerException("action to perform is null"); int myIdx = idx.getAndIncrement(); if (myIdx < results.length) { action.accept(results[myIdx]); return true; } synchronized (this) { if (next != null) { return next.join().tryAdvance(action); } } return false; } /** * If this spliterator can be partitioned, returns a Spliterator * covering elements, that will, upon return from this method, not * be covered by this Spliterator. * <p> * <p>If this Spliterator is {@link #ORDERED}, the returned Spliterator * must cover a strict prefix of the elements. * <p> * <p>Unless this Spliterator covers an infinite number of elements, * repeated calls to {@code trySplit()} must eventually return {@code null}. * Upon non-null return: * <ul> * <li>the value reported for {@code estimateSize()} before splitting, * must, after splitting, be greater than or equal to {@code estimateSize()} * for this and the returned Spliterator; and</li> * <li>if this Spliterator is {@code SUBSIZED}, then {@code estimateSize()} * for this spliterator before splitting must be equal to the sum of * {@code estimateSize()} for this and the returned Spliterator after * splitting.</li> * </ul> * <p> * <p>This method may return {@code null} for any reason, * including emptiness, inability to split after traversal has * commenced, data structure constraints, and efficiency * considerations. * * @return a {@code Spliterator} covering some portion of the * elements, or {@code null} if this spliterator cannot be split * @apiNote An ideal {@code trySplit} method efficiently (without * traversal) divides its elements exactly in half, allowing * balanced parallel computation. Many departures from this ideal * remain highly effective; for example, only approximately * splitting an approximately balanced tree, or for a tree in * which leaf nodes may contain either one or two elements, * failing to further split these nodes. However, large * deviations in balance and/or overly inefficient {@code * trySplit} mechanics typically result in poor parallel * performance. */ @Override public Spliterator<String> trySplit() { synchronized (this) { // If we have no next element readily on hand, then just don't do the split. if (this.next == null) return null; // Get a handle on the next spliterator: this will block until it resolves ScanSpliterator nextSpliterator = this.next.join(); // For something like balance, try to advance 50% of the times when we can // (This may involve additional blocking...) if (nextSpliterator.next != null && ThreadLocalRandom.current().nextBoolean()) { Spliterator<String> toReturn = nextSpliterator.trySplit(); if (toReturn != null) return toReturn; } // We either can't or don't want to advance; use our next value as a spliterator this.next = null; return nextSpliterator; } } /** * Returns an estimate of the number of elements that would be * encountered by a {@link #forEachRemaining} traversal, or returns {@link * Long#MAX_VALUE} if infinite, unknown, or too expensive to compute. * <p> * <p>If this Spliterator is {@link #SIZED} and has not yet been partially * traversed or split, or this Spliterator is {@link #SUBSIZED} and has * not yet been partially traversed, this estimate must be an accurate * count of elements that would be encountered by a complete traversal. * Otherwise, this estimate may be arbitrarily inaccurate, but must decrease * as specified across invocations of {@link #trySplit}. * * @return the estimated size, or {@code Long.MAX_VALUE} if infinite, * unknown, or too expensive to compute. * @apiNote Even an inexact estimate is often useful and inexpensive to compute. * For example, a sub-spliterator of an approximately balanced binary tree * may return a value that estimates the number of elements to be half of * that of its parent; if the root Spliterator does not maintain an * accurate count, it could estimate size to be the power of two * corresponding to its maximum depth. */ @Override public long estimateSize() { if (this.next == null) return getCurrentSize(); if (!this.next.isCompletedNormally()) return (this.getCurrentSize() + 1) * 2; return ((long) this.getCurrentSize()) + this.next.join().getCurrentSize(); } /** * Gets the current size for this specific spliterator, not including any subsequent spliterators. * * @return The current size for this specific spliterator. */ protected int getCurrentSize() { return Math.max(0, this.idx.get() - this.results.length + 1); } /** * Returns a set of characteristics of this Spliterator and its * elements. The result is represented as ORed values from {@link * #ORDERED}, {@link #DISTINCT}, {@link #SORTED}, {@link #SIZED}, * {@link #NONNULL}, {@link #IMMUTABLE}, {@link #CONCURRENT}, * {@link #SUBSIZED}. Repeated calls to {@code characteristics()} on * a given spliterator, prior to or in-between calls to {@code trySplit}, * should always return the same result. * <p> * <p>If a Spliterator reports an inconsistent set of * characteristics (either those returned from a single invocation * or across multiple invocations), no guarantees can be made * about any computation using this Spliterator. * * @return a representation of characteristics * @apiNote The characteristics of a given spliterator before splitting * may differ from the characteristics after splitting. For specific * examples see the characteristic values {@link #SIZED}, {@link #SUBSIZED} * and {@link #CONCURRENT}. */ @Override public int characteristics() { return Spliterator.NONNULL | Spliterator.IMMUTABLE | Spliterator.CONCURRENT; } }